Files
goose/ARCHITECTURE.md
2025-03-31 10:17:24 -07:00

177 lines
6.0 KiB
Markdown

# Architecture
## The Extension System
Goose extends the capabilities of high-performing LLMs through a small collection of tools.
This lets you instruct goose, currently via a CLI interface, to automatically solve problems
on your behalf. It attempts to not just tell you how you can do something, but to actually do it for you.
The primary mode of goose (the "developer" extension) has access to tools to
- maintain a plan
- run shell commands
- read, create, and edit files
Together these can solve all kinds of problems, and we emphasize performance on tasks like
fully automating adhoc scripts and tasks, interacting with existing code bases, and teaching how
to use new technology.
---
Here are some of the key design decisions about how we drive performance on these tasks with goose,
that you should be able to observe by using it.
- Encouraging it to write and maintain a plan, to allow it to accomplish longer sequences of automation
- Using tool usage as a generalizable and increasingly tuned approach to adding new capabilities (including plugins)
- Relying on reflection at every possible part of the stack
- Showing it clear output of each tool use
- Surfacing all possible errors to the model to give it a chance to correct
- Surfacing the plan to document what has been accomplished
> [!TIP]
> In addition, there are some implementation choices that we've found very performance driving. They share
> a theme of working well by default without constraining the model.
>
> - Encouraging the model to use `ripgrep` via the shell performs very well for navigating filesystems. It mostly
> just works, but enables the model to get clever with regexes or even additional shell operations as needed.
> - Using a replace operation for editing files requires fewer tokens to be generated and avoids laziness on large files,
> but we allow fall back to whole file overwrites to let it more coherently handle major refactors.
## Implementation
The core execution logic for generation and tool calling is handled by [exchange][exchange].
It hooks rust functions into the model tool use loop, while defining very careful error handling
so any failures in tools are surfaced to the model.
Once we've created an *exchange* object, running the process is effectively just calling
`exchange.reply()`.
*The key is setting up an exchange with the capabilities we need.*
Goose builds that exchange:
- allows users to configure a profile to customize capabilities
- provides a pluggable extension system for adding tools and prompts
- sets up the tools to interact with state
We expect that goose will have multiple UXs over time, and be run in different
environments. The UX is expected to be able to load a `Profile` (e.g. in the CLI
we read profiles out of a config) and to provide a `Notifier` (e.g. in the CLI we put
notifications on stdout).
Goose then constructs the exchange for the UX, the UX only interacts with that exchange.
```rust
fn build_exchange(profile: Profile, notifier: Notifier) -> Exchange {
...
}
```
But to setup a configurable system, Goose uses `Extensions`:
```
(Profile, Notifier) -> [Extensions] -> Exchange
```
## Profile
A profile specifies some basic configuration in Goose, such as which models it should use, as well
as which extensions it should include.
```yaml
processor: openai:gpt-4
accelerator: openai:gpt-4-turbo
moderator: passive
extensions:
- developer
- calendar
- contacts
- name: scheduling
requires:
assistant: assistant
calendar: calendar
contacts: contacts
```
## Notifier
The notifier is a concrete implementation of the Notifier trait provided by each UX. It
needs to support two methods:
```rust
trait Notifier {
fn log(&self, content: RichRenderable);
fn status(&self, message: String);
}
```
Log is meant to record something concrete that happened, such as a tool being called, and status is intended
for transient displays of the current status. For example, while a shell command is running, it might use
`.log` to record the command that started, and then update the status to `"shell command running"`. Log is durable
while Status is ephemeral.
## Extensions
Extensions are a collection of tools, along with the state and prompting they require.
Extensions are what gives Goose its capabilities.
Tools need a way to report what's happening back to the user, which we treat similarly
to logging. To make that possible, extensions get a reference to the interface described above.
```rust
struct ScheduleExtension {
notifier: Box<dyn Notifier>,
calendar: Box<dyn Calendar>,
assistant: Box<dyn Assistant>,
contacts: Box<dyn Contacts>,
appointments_state: Vec<Appointment>,
}
impl Extension for ScheduleExtension {
fn new(notifier: Box<dyn Notifier>, requires: Requirements) -> Self {
Self {
notifier,
calendar: requires.get("calendar"),
assistant: requires.get("assistant"),
contacts: requires.get("contacts"),
appointments_state: vec![],
}
}
fn prompt(&self) -> String {
"Try out the example tool.".to_string()
}
#[tool]
fn example(&self) {
self.notifier.log(format!("An example tool was called, current state is {:?}", self.appointments_state));
}
}
```
### Advanced
**Dependencies**: Extensions can depend on each other, to make it easier to get plugins to extend
or modify existing capabilities. In the config above, you can see this used for the scheduling extension.
You can refer to those requirements in code through:
```rust
#[tool]
fn example_dependency(&self) {
let appointments = self.calendar.appointments();
// ...
}
```
**ExchangeView**: It can also be useful for tools to have a read-only copy of the history
of the loop so far. So for advanced use cases, extensions also have access to an
`ExchangeView` object.
```rust
#[tool]
fn example_history(&self) {
let last_message = self.exchange_view.processor.messages.last();
// ...
}
```
[exchange]: https://github.com/block/goose/tree/main/packages/exchange