# goose source analysis: a local goose that knows how to use tools

> **Subject**: `aaif-goose/goose` (the open-source fork of Block’s goose, now under the Linux Foundation Agentic AI Foundation)
> **Based on commit**: `d17d65f2f30876e391395e1e8b5f2666320588c6` (2026-07-23, `feat: support latest Gemini models (#10630)`)
> **Analysis date**: 2026-07-24
> **Size**: Rust about 463 `.rs` files (Cargo workspace, 12 crates) + TypeScript 228 `.ts` / 361 `.tsx` (Electron desktop etc.), workspace version `1.44.0` (`Cargo.toml:11`)

This document is for a computer-science beginner. We will walk this codebase the way you walk a zoo: first the map (architecture), then each animal’s habits (modules), finally the design philosophy of this “goose” (goose literally means goose). Every `file:line` citation truly exists in the commit above; you can follow the map.

---

## 1. Project overview: what is goose?

goose is a **general-purpose AI agent that runs on your own computer**. The README is blunt:

> "goose is a general-purpose AI agent that runs on your machine. Not just for code — use it for research, writing, automation, data analysis, or anything you need to get done." (`README.md:23`)

Key facts:

- **Who builds it**: originally Block (parent of Square/Cash App); the author email is still in `Cargo.toml:13` (`ai-oss-tools@block.xyz`). Now a project of the Linux Foundation’s **Agentic AI Foundation (AAIF)** (`README.md:29`); governance is in `GOVERNANCE.md`—lightweight technical governance, core values **Open / Flexible / Choice**.
- **Three shapes**: a **desktop app** for macOS/Linux/Windows (Electron), a full **CLI** (Rust), and an **API** that can be embedded elsewhere (`README.md:25`).
- **Stack**: all core logic in **Rust** (one Cargo workspace, 12 crates); desktop is **Electron + React + TypeScript**; plus an experimental terminal UI written with Ink (React rendered onto a terminal character grid) (`ui/text`, see `AGENTS.md:88`).
- **Model-neutral**: 15+ providers—Anthropic, OpenAI, Google, Ollama, OpenRouter, Azure, Bedrock, etc. (`README.md:27`); can also reuse your existing Claude / ChatGPT / Gemini subscriptions via **ACP** (Agent Client Protocol).
- **Extension via MCP**: 70+ extensions connected through the open standard **Model Context Protocol (MCP)** (`README.md:27`).

**One-sentence contrast with Claude Code**: Claude Code is Anthropic’s official TypeScript terminal agent, bound to Claude models; goose is a **model-neutral, Rust-written open-source agent**, the same core driving both CLI and desktop GUI, the tool ecosystem built entirely on the MCP open protocol. Both read project prompt files such as `AGENTS.md`/`CLAUDE.md`, both have slash commands, permission modes, and context compaction, but goose fully externalizes “tools” into “extensions” and turns workflows into shareable “recipes.”

```mermaid
flowchart LR
    U[用户] -->|打字| CLI["goose CLI<br/>(Rust)"]
    U -->|点击| GUI["Goose Desktop<br/>(Electron)"]
    U -->|HTTP/WS| API["goose serve<br/>(ACP Server)"]
    CLI --> CORE["crates/goose<br/>核心库 (Agent)"]
    GUI --> API
    API --> CORE
    CORE -->|调用| LLM["15+ 模型提供商<br/>Anthropic/OpenAI/Ollama..."]
    CORE -->|MCP 协议| EXT["70+ 扩展<br/>文件/浏览器/数据库..."]
    style CORE fill:#ffe4b5,stroke:#333
```

---

## 2. Panorama: one Rust core, three shells

Open the repo root; the two most important folders are `crates/` (Rust source) and `ui/` (TypeScript source). `Cargo.toml:2-6` declares workspace members as `crates/*`, 12 crates:

| crate | Duty |
|---|---|
| `goose` | **the heart**: Agent loop, extension management, prompts, compaction, permissions, recipes, scheduler |
| `goose-cli` | CLI shell: command parsing, interactive sessions, rendering |
| `goose-mcp` | MCP servers bundled with the goose (memory, computercontroller, etc.) |
| `goose-providers` / `goose-provider-types` | model-provider implementations and shared types |
| `goose-sdk` / `goose-sdk-types` | SDK for embedding in external programs |
| `goose-acp-macros` | ACP protocol proc macros |
| `goose-local-inference` / `goose-download-manager` | local inference (llama.cpp/candle) and model download |
| `goose-test` / `goose-test-support` | test tools |

Under `ui/` there are `desktop` (Electron desktop app), `text` (Ink terminal UI), `goose-binary`, `sdk`, etc.

**Key design: CLI and Desktop share the same `goose` core crate, but not the same way.**

- CLI links the `goose` library directly and calls `Agent::reply()` in-process.
- Desktop does not link the Rust library; it **spawns a `goose serve` child process**—an ACP-over-HTTP/WebSocket server (`crates/goose-cli/src/cli.rs:844`, "Start ACP server over HTTP and WebSocket"). Electron main process `ui/desktop/src/gooseServe.ts:320` starts it; the frontend then talks to it over WebSocket with ACP.

In other words: **goose made the “agent engine” a local server; the desktop GUI is only one of its clients**. This is spiritually similar to Claude Code’s “SDK + terminal UI,” but goose goes further—even its own GUI talks over a protocol, which means any third-party program can “ride” this goose with the same protocol.

```mermaid
flowchart TD
    subgraph 外壳层
        A["goose-cli<br/>(crates/goose-cli)"]
        B["Goose Desktop<br/>(ui/desktop, Electron)"]
        C["第三方程序<br/>(Zed 等编辑器)"]
    end
    subgraph 协议层
        D["直接函数调用"]
        E["ACP over WebSocket/HTTP<br/>goose serve"]
    end
    subgraph 核心层 crates/goose
        F["Agent 主循环<br/>agents/agent.rs"]
        G["ExtensionManager<br/>扩展管理"]
        H["PromptManager<br/>提示词组装"]
        I["权限/安全/压缩"]
    end
    subgraph 外部世界
        J["Provider API<br/>(Anthropic, OpenAI...)"]
        K["MCP 扩展进程"]
    end
    A --> D --> F
    B --> E --> F
    C --> E --> F
    F --> G --> K
    F --> J
    F --> H
    F --> I
```

---

## 3. Startup: from binary to conversation UI

### CLI side

The entry is tiny: `main()` in `crates/goose-cli/src/main.rs:34`. What it does can be summarized as “build the stage, then sing the opera”:

```rust
// main.rs:38-48（简化注释）
let handle = std::thread::Builder::new()
    .name("goose-cli-main".to_string())
    .stack_size(8 * 1024 * 1024)          // 8MB 栈，防深层递归爆栈
    .spawn(|| {
        let runtime = tokio::runtime::Builder::new_multi_thread()
            .enable_all().build().unwrap();
        runtime.block_on(run())            // 在新线程里跑 tokio 异步运行时
    })?;
```

Why a dedicated thread? Because when the agent recursively processes messages the call stack can be very deep; the default thread stack (usually 2MB) is not enough. After initializing logs, `run()` calls `cli()` (`crates/goose-cli/src/cli.rs:2205`), which uses **clap** to parse the command line into a huge `Command` enum (from `cli.rs:803`)—`session`, `run`, `recipe`, `schedule`, `mcp`, `acp`, `serve`, `configure`, `doctor`……

If you type `goose` with no subcommand, you land on `None => handle_default_session().await` at `cli.rs:2378`, i.e. `cli.rs:2165`:

```rust
async fn handle_default_session() -> Result<()> {
    if !Config::global().exists() {
        return handle_configure().await;      // 第一次用？先进入配置向导
    }
    // ...
    let session_id = get_or_create_session_id(...).await?;
    let mut session = build_session(SessionBuilderConfig { interactive: true, ... }).await;
    session.interactive(None).await            // 进入交互式对话循环
}
```

`build_session` creates an `Agent`, loads extensions enabled in config (each extension may be a child process), then `session.interactive()` enters the input loop of the next chapter.

### Desktop side

Desktop entry is Electron main process `ui/desktop/src/main.ts`. It does window management, menus (including Chinese menu translations, from `main.ts:73`), auto-update, etc., then starts the backend via `startGooseServe` (`ui/desktop/src/gooseServe.ts:320`):

```mermaid
sequenceDiagram
    participant E as Electron 主进程 (main.ts)
    participant S as goose serve 子进程
    participant R as React 渲染进程
    E->>S: spawn "goose serve --port N"<br/>(gooseServe.ts:320)
    S-->>E: 就绪（状态 URL 可访问）
    E->>R: 创建 BrowserWindow
    R->>S: WebSocket 连接（ACP 协议）
    Note over R,S: 之后的对话/工具调用全走 ACP
```

Worth mentioning: Desktop and backend require `GOOSE_SERVER__SECRET_KEY` auth (`gooseServe.ts:336`), preventing other programs on the machine from casually attaching to this server that can execute shell commands—part of the safety design.

**CLI startup has a hidden branch worth knowing**: the first thing `handle_default_session` does is check whether a config file exists (`cli.rs:2166-2168`); if not, it goes straight into the interactive config wizard `handle_configure()`—so a new user’s first `goose` is not an error, it is a “welcome, please pick your model provider” guide. This “diagnose first, then prescribe” order keeps beginners from facing a pile of environment variables with no idea what to do.

---

## 4. Input capture and routing: slash commands are “local shortcuts”

### CLI: rustyline line by line

CLI input is owned by **rustyline** (a readline-style Rust library); entry is `get_input()` in `crates/goose-cli/src/session/input.rs:117`. After reading a line, a key routing (`input.rs:198-218`):

1. If it **does not** start with `/` → wrap as `InputResult::Message`, send to the agent;
2. If it starts with `/` → hand to `handle_slash_command()` (`input.rs:221`) to match built-in commands.

All possible results are defined in the `InputResult` enum at `input.rs:15-36`:

```rust
pub enum InputResult {
    Message(String),        // 普通消息 → 发给模型
    Exit,                   // /exit 或 Ctrl+C 两次
    AddExtension(String),   // /extension <cmd>
    GooseMode(String),      // /mode auto|approve|smart_approve|chat
    Model(Option<String>),  // /model [名字]
    Plan(PlanCommandOptions), EndPlan,  // /plan 与 /endplan
    Clear, Compact,         // /clear 清屏、/compact 手动压缩
    Recipe(Option<String>), // /recipe
    ListSkills, LoadSkills(Vec<String>), // /skills
    // ... 还有主题切换、重试、编辑等
}
```

`/compact` also has an alias mechanism: `COMPACT_TRIGGERS` (`crates/goose/src/agents/execute_commands.rs:11`). A very human detail is **Ctrl+C’s double meaning** (`input.rs:60-87`): if the current line has text, Ctrl+C only clears the input line; on an empty line the first press shows “press again to exit,” the second actually exits—avoids losing a session by a slip.

**A routing example**: you type `/mode smart_approve`; it is not sent to the model, it becomes `InputResult::GooseMode("smart_approve")`, and the CLI locally switches permission mode; you type `help me translate the README into Chinese`, and it enters the agent loop as `Message`.

**Two more examples covering different cases**: first, an unknown command such as `/dance`—`handle_slash_command` matches nothing and returns `None`, so the whole string is sent to the model as-is—the model will likely answer “I don’t know what /dance is,” rather than the CLI erroring; that lets future commands fall back on the model. Second, pasting a large error log with newlines: paste-aware input in `paste.rs` (`read_paste_aware_input`, imported at `input.rs:2-3`) treats the whole paste as one input rather than triggering line by line, avoiding empty lines in the log being misread as “send.”

### Desktop / ACP side

The desktop slash-command list is provided by `list_acp_commands()` in `crates/goose/src/slash_commands/slash_command.rs:20`; it merges three sources and dedups (`slash_command.rs:30-53`), priority **built-in > recipe > skill**:

- **Built-in commands** (`execute_commands.rs:21-56`): `prompts`, `prompt`, `compact`, `clear`, `skills`, `doctor`, `goal`, `grind`, `status`;
- **Recipe commands**: shortcuts exposed by the currently loaded recipe;
- **Skill commands**: SKILL.md skills found on disk.

```mermaid
flowchart TD
    I[用户输入一行文字] --> Q{以 / 开头?}
    Q -->|否| M["InputResult::Message<br/>→ agent.reply() 主循环"]
    Q -->|是| S{匹配内置斜杠命令?}
    S -->|/exit, /quit| E[退出会话]
    S -->|/mode X| G[本地切换 GooseMode 权限模式]
    S -->|/model X| MO[切换模型]
    S -->|/compact| C[手动触发上下文压缩]
    S -->|/plan| P[进入计划模式]
    S -->|未匹配| M2["当作普通消息发给模型<br/>(让模型自己理解)"]
```

---

## 5. Context assembly: how the system prompt is “plated”

Before every model request, goose has to plate a **system prompt**. The assembly shop is `crates/goose/src/agents/prompt_manager.rs`; the template is `crates/goose/src/prompts/system.md` (minijinja-style template syntax).

### What is in the template

`system.md` opens with a self-introduction (`system.md:1`):

> "You are a general-purpose AI agent called goose, created by AAIF (Agentic AI Foundation)."

Then conditionally inserts: a MOIM explanation block, the **extension list** (`{% for extension in extensions %}`, each extension’s name, whether it supports resources, usage notes), a suggestion when there are too many extensions (remind the user to slim down when more than 5 extensions or 50 tools, `prompt_manager.rs:19-20`), and output norms such as “reply in Markdown.”

Assembly entry is `build()` at `prompt_manager.rs:117`; it collects a `SystemPromptContext` (`prompt_manager.rs:36-49`): extension info, current time, goose mode, whether autonomous, whether sub-agents are enabled, etc. Note two details born for **prompt cache**:

- Timestamp **only precise to the hour** (`prompt_manager.rs:207-209`: `%Y-%m-%d %H:00`)—so system prompts of multiple requests in the same hour are identical, can hit the model provider’s cache, saving money and time;
- Extension info **sorted by name** (`prompt_manager.rs:129`); the comment says "Stable tool ordering is important for multi session prompt caching".

### Hint files: .goosehints and AGENTS.md

goose’s project-level “whisper” file rules live in `crates/goose/src/hints/load_hints.rs`:

- By default collect two filenames: **`.goosehints`** and **`AGENTS.md`** (`load_hints.rs:10-11`); the config item `CONTEXT_FILE_NAMES` can change them to any filename (`load_hints.rs:13-24`)—for example make it read `CLAUDE.md`; there is a test for this (`load_hints.rs:458-468`).
- **Global layer**: same-named files under the config directory, plus `~/.agents/AGENTS.md` (`load_hints.rs:233-242`).
- **Project layer**: from the git root all the way down to the current working directory, every level’s hint file is collected (`load_hints.rs:263-285`); if it is not a git repo, only look at the current directory (test `load_hints.rs:505-535` verifies this).
- **Supports `@file` import**: writing `@docs/api.md` in a hint file inlines that file’s content, but **the import boundary is the git root**—an out-of-bound reference like `@../../../forbidden.md` is refused (test `load_hints.rs:635-688`), and files ignored by `.gitignore` (e.g. `secret.env`) are not read even if `@`’d (test `load_hints.rs:856-879`). This is an important ring against prompt-injection leaking secrets.
- **Subdirectory hints loaded dynamically**: `SubdirectoryHintTracker` (`load_hints.rs:26-96`) watches tool-call arguments—if the agent `write`s `nested/foo.rs` or shells `cat nested/doc.md`, it records `nested/` as a “directory to check”; next round if a `.goosehints` is found there it is auto-appended to the system prompt (the agent loop at `agent.rs:2666-2676` rebuilds the prompt when new hints are found).

Collected hints are appended to the system prompt’s `# Additional Instructions:` section as `### Global Hints` / `### Project Hints` subheadings (`prompt_manager.rs:193-198`).

**A layered-stacking example**: your home config directory has a global `.goosehints` (saying “reply in Chinese”), the git repo root has an `AGENTS.md` (saying “this is a Rust project, after changing you must `cargo clippy`”), you are currently working under `src/utils/` and that directory has another `.goosehints` (saying “this directory is performance-sensitive code”)—the final system prompt contains all three, and the model sees global preference, project rules, and local cautions at a glance. This is almost identical to how Claude Code collects `CLAUDE.md`; goose just additionally gives you the freedom to custom-name the files.

### Each round also stuffs a “dynamic sticky note”: MOIM

The system prompt is “static” (for cache); information that changes each round takes another path—`inject_moim()` in `crates/goose/src/agents/moim.rs` (called every round by the agent loop at `agent.rs:2026-2032`). It plates current time, working directory, token usage/compaction state, **remaining turn budget**, and context provided by each extension into a `<turn_context>` text block, **inserted into the last user message, before tool responses** (`moim.rs:97-111`). The system prompt tells the model in advance “this block is operational context, not a user request” (`moim.rs:11-27`), and teaches it “when the budget is almost gone, explore less, wrap up fast.” Models with a context window under 32k tokens skip MOIM entirely (`moim.rs:9`)—a small window cannot afford this extra cost.

```mermaid
flowchart TD
    subgraph 系统提示词 system prompt
        A["system.md 模板<br/>(身份+扩展说明)"]
        B[".goosehints / AGENTS.md<br/>全局+项目层, 支持 @导入"]
        C["frontend 指令<br/>(桌面端注入)"]
        D["模式提示<br/>chat 模式禁用工具"]
    end
    subgraph 每轮消息 messages
        E["历史对话<br/>(压缩后)"]
        F["MOIM turn_context 便签<br/>时间/目录/预算/token 用量"]
        G["用户最新消息"]
    end
    A & B & C & D --> H["provider.stream(<br/>system_prompt, messages, tools<br/>reply_parts.rs:287)"]
    E & F & G --> H
```

---

## 6. The agent loop (the heart): `reply_internal` in one breath

The most important stretch of code in all of goose is `reply_internal()` at `crates/goose/src/agents/agent.rs:1846`. The outward entry is `reply()` at `agent.rs:1549` (handles elicitation responses, appending user messages, etc., then calls in). The loop body is the `loop { ... }` at **`agent.rs:1948`**.

First a “heartbeat” panorama, then beat by beat:

```mermaid
flowchart TD
    START["用户消息到达 reply()<br/>agent.rs:1549"] --> PRE["prepare_reply_context<br/>修会话+拼提示词+取工具<br/>agent.rs:734"]
    PRE --> LOOP{"主循环<br/>agent.rs:1948"}
    LOOP -->|已取消?| OUT1[CancellationToken → 退出]
    LOOP --> STEER["排出插队消息 drain_pending_steers<br/>agent.rs:1953"]
    STEER --> TURN["turns_taken += 1<br/>超过 max_turns(默认1000) 就收尾<br/>agent.rs:2018-2024"]
    TURN --> MOIM["注入 MOIM 动态上下文<br/>agent.rs:2026"]
    MOIM --> REQ["stream_response_from_provider<br/>请求模型(流式)<br/>reply_parts.rs:287"]
    REQ --> PARSE{"流式解析响应<br/>agent.rs:2079"}
    PARSE -->|纯文本,无工具| TXT["追加 assistant 消息<br/>no_tools_called=true"]
    PARSE -->|有工具调用| CAT["categorize_tools 分类<br/>agent.rs:2124"]
    CAT --> PERM["权限检查器逐个裁决<br/>agent.rs:2210-2232"]
    PERM --> EXEC["执行工具(并发)<br/>前端工具/已批准/待审批<br/>agent.rs:2244-2336"]
    EXEC --> APPEND["请求+结果成对追加进会话<br/>agent.rs:2458-2524"]
    APPEND --> LOOP
    TXT --> CHECK{"终止条件?<br/>agent.rs:2697+"}
    CHECK -->|final_output 已提交| DONE[结束循环]
    CHECK -->|设定了 goal/grind| NUDGE["塞一条隐形催促消息<br/>agent.rs:2720-2755"] --> LOOP
    CHECK -->|空响应| RETRY["最多重试 3 次<br/>agent.rs:2778"] --> LOOP
    CHECK -->|否则| DONE
    PARSE -->|ContextLengthExceeded| COMPACT["compact_messages 压缩<br/>agent.rs:2562"] --> LOOP
```

Beat by beat:

1. **Cancel check** (`agent.rs:1949`): at the start of each round look at `CancellationToken`—this is the brake when the user presses ESC/Ctrl+C. During tool execution it is likewise listened to inside `tokio::select!` (`agent.rs:2276-2281`).
2. **Steer messages**: the user can still append messages while the agent works; `drain_pending_steers` (`agent.rs:1953-1973`) queues them into the session and also fires a `UserPromptSubmit` hook.
3. **Turn counting**: `turns_taken` cap defaults to `DEFAULT_MAX_TURNS = 1000` (`agent.rs:69`), also configurable with `GOOSE_MAX_TURNS` (`agent.rs:1931-1935`). Over the cap, output `MAX_TURNS_MESSAGE` and wrap up.
4. **Request the model**: `stream_response_from_provider` (`reply_parts.rs:287`) first does two preprocesses—project the session into messages “visible to the agent” and fix format issues (`reply_parts.rs:298-301`), then call `provider.stream(model_config, system_prompt, messages, tools)` (`reply_parts.rs:322-330`). If the model does not support native tool calling (toolshim mode), stuff tool descriptions into the prompt, let the model emit JSON markers, then parse locally (`reply_parts.rs:269-278, 304-308`)—a “crutch” for small models.
5. **Stream parse**: `while let Some(next) = stream.next().await` (`agent.rs:2079`) reads chunk by chunk. Pure text/thinking content is `yield AgentEvent::Message` pushed to the UI; tool calls go through `categorize_tools` (`agent.rs:2124`) into two classes: **frontend tools** (executed by the frontend/Desktop, e.g. “show a chart on screen”) and **ordinary tools**.
6. **Tool execution**: in Chat mode every tool request is answered with a “skipped” (`agent.rs:2191-2207`, `CHAT_MODE_TOOL_SKIPPED_RESPONSE`); other modes first pass the **tool inspector** (permissions, security scan, see Chapter 7), splitting requests into approved / needs_approval / denied three piles (`agent.rs:2219-2232`), then execute concurrently (`stream::select_all`, `agent.rs:2273`). Each tool request is pre-paired with a placeholder response message (`request_to_response_map`, `agent.rs:2172-2177`); when execution finishes the result is filled back in.
7. **Append and request again**: tool-request messages and response messages are appended **in pairs** (`agent.rs:2458-2524`), then `loop` returns to the top and requests the model again—the classic “request → tool → append → request again” agent loop. Note the large stretch at `agent.rs:2346-2456` handling **thinking (reasoning) content**: it hangs reasoning blocks onto the tool-call message rather than storing them separately; the comment explains why—Anthropic’s signed thinking blocks are 400-refused by the API if they appear twice, while Gemini/Kimi/DeepSeek require thinking content to be echoed back—an engineering detail squeezed out by being “pincered” by several models.
8. **Termination conditions**: when a round called no tools (`agent.rs:2697`), check in order: final_output tool (forced when a recipe requires structured output, `agent.rs:2705-2714`) → whether there are steer messages → **whether a goal/grind goal is met** (if not, stuff a user-invisible, model-visible nudge so the agent keeps working, `agent.rs:2720-2755`) → recipe retry logic → empty-response fallback retry (at most `MAX_EMPTY_TURN_RETRIES = 3` times, `agent.rs:73, 2778-2785`). If none of those, the loop truly ends.
9. **Context-overflow self-heal**: if the provider reports `ContextLengthExceeded` (`agent.rs:2532`), first tell the user “compacting,” call `compact_messages` to compact the session, then **continue the loop with the compacted session**; if one compaction is not enough (`compaction_attempts >= 2`) then give up (`agent.rs:2538-2547`). Other errors (insufficient balance `CreditsExhausted`, refusal `Refusal`, network errors) each have dedicated branches, prompting recharge, suggesting a new session, suggesting resend (`agent.rs:2592-2656`).

**Interrupt mechanism in brief**: the cancel token runs through three layers—start of the agent loop, inside stream reading, and the `select!` of concurrent tool execution—so pressing interrupt at any time can stop reasonably quickly.

**A complete “heartbeat” instance**: suppose you say “run the tests and fix the one that failed.” Beat one: the model returns text “I’ll run tests first” plus a `shell {command: "cargo test"}` tool call; `categorize_tools` puts it in ordinary tools; the permission inspector allows it then concurrent execute; stdout is filled into the placeholder response message, appended in a pair with the request. Beat two: the model reads the test output, says “an assertion failed, I’ll change it,” and emits `edit {path: "src/lib.rs", before: "...", after: "..."}`; after success, loop again. Beat three: the model runs tests once more to confirm pass, then returns only text “fixed, the failure was……” — this round has no tool calls (`no_tools_called = true`), all termination checks pass, the loop ends, control returns to you. Throughout, what you see on the UI is streaming character-by-character text and tool-call cards; behind it this one `loop` is breathing.

---

## 7. Tools and permissions: everything is an extension

In goose there are **no built-in tools, only built-in extensions**. Every capability—even reading and writing files—is a tool some extension provides over MCP.

### Six shapes of extension

The `ExtensionConfig` enum at `crates/goose/src/agents/extension.rs:162` defines how extensions attach:

| Variant | Meaning |
|---|---|
| `Stdio` | start a local child process, speak MCP over stdin/stdout (most common) |
| `Sse` / `StreamableHttp` | connect to a remote MCP server (SSE already marked unsupported, `extension.rs:420`) |
| `Builtin` | MCP servers that ship with goose, speaking MCP **in-process** over an in-memory pipe (`goose-mcp` crate) |
| `Platform` | platform extensions: written in Rust directly in the agent process, can access agent internals |
| `Frontend` | tools provided by the frontend (Desktop); calls must be forwarded to the UI to execute |
| `InlinePython` | inline Python code extensions |

`ExtensionManager` (`agents/extension_manager.rs`) hatches them: stdio extensions use `Command::new(cmd).spawn()` (`extension_manager.rs:431-432`), also supporting `uvx` (Python-family extensions) and `docker` wrapping (`extension_manager.rs:1035-1146`). The exposure rule for tool names is **`extension_name__tool_name`** double-underscore prefix (`extension_manager.rs:1425`), e.g. `memory__remember_memory`; a few “first-class citizen” extensions (developer, analyze, summon, skills) are configured `unprefixed_tools: true`, tools using bare names, e.g. `shell`, `write`.

### Full built-in extension and tool inventory

**Platform extensions** (written under `crates/goose/src/agents/platform_extensions/`, registry `mod.rs:28-207`):

| Extension | Default on | Tools (prefix omitted) | Function | Example |
|---|---|---|---|---|
| **developer** | ✅ | `shell` / `write` / `edit` / `tree` / `read_image` | run shell, write/edit files, see directory tree, read images (`developer/mod.rs:101-172`) | `shell {command: "cargo test"}` |
| **analyze** | ✅ | `analyze` | analyze code structure and symbol call graphs with tree-sitter (`analyze/mod.rs:221`) | `analyze {path: "src/"}` |
| **todo** | ✅ | `todo_write` | maintain a task list (`todo.rs:117`) | note “1. change code 2. run tests” |
| **summon** | ✅ | `load` / `delegate` | load knowledge sources; spawn a sub-agent (`summon.rs:576, 649`) | Chapter 9 |
| **skills** | ✅ | `load_skill` | load a skill file’s full text (`skills/client.rs:82`) | load a “write a PPT” skill |
| **extensionmanager** | ✅ | `search_available_extensions` / `manage_extensions` | let the agent itself search, enable, disable extensions (`ext_manager.rs:269-344`) | “open the browser extension for me” |
| **apps** | ✅ | `list_apps` / `create_app` / `iterate_app` / `delete_app` | generate sandboxed HTML mini-apps (`apps.rs:521-538`) | “make a pomodoro clock” |
| **tom** (Top Of Mind) | ✅ | no tools | inject custom context into every round via env `GOOSE_MOIM_MESSAGE_TEXT/FILE` (`mod.rs:178-190`) | inject a ticket number in CI |
| **chatrecall** | ❌ | `chatrecall` | search historical sessions, load summaries (`chatrecall.rs:257`) | “how did we configure nginx last time?” |
| **summarize** | ❌ | `summarize` | one-shot “read files + LLM summary” (`summarize.rs:74`) | quickly digest a large directory |
| **code execution** (feature: code-mode) | ❌ | `list_functions` / `get_function_details` / `execute_typescript` / `execute_bash` | turn tool calls into writing code, saving tokens (`code_execution.rs:454-522`) | string 5 tool calls with a TS snippet |
| **orchestrator** (hidden) | ❌ | `list_sessions` / `view_session` / `start_agent` / `send_message` / `interrupt_agent` | manage multiple agent sessions (`orchestrator.rs:593-619`) | Chapter 9 |

**Bundled MCP extensions** (`crates/goose-mcp/`, registry `goose-mcp/src/lib.rs:57-64`):

| Extension | Tools | Function |
|---|---|---|
| **memory** | `remember_memory` / `retrieve_memories` / `remove_memory_category` / `remove_specific_memory` (`memory/mod.rs:335-432`) | cross-session long-term memory, store/retrieve by category + tags |
| **computercontroller** | `web_scrape` / `automation_script` / `computer_control` / `xlsx_tool` / `docx_tool` / `pdf_tool` / `cache` (`computercontroller/mod.rs`) | web scrape, system automation, office-document processing |
| **autovisualiser** | `render_sankey/radar/donut/treemap/chord/map/mermaid` / `show_chart` (`autovisualiser/mod.rs:892-1319`) | generate charts and display them |
| **tutorial** | `load_tutorial` (`tutorial/mod.rs:88`) | beginner tutorial |
| **peekaboo** (macOS only, conditionally compiled module, **not in the BUILTIN_EXTENSIONS registry**) | — | conditionally compiled at `lib.rs:18-19`, but `BUILTIN_EXTENSIONS` at `lib.rs:57-64` only registers 4 (no peekaboo) |

**What does a tool look like? Read `shell` line by line** (`developer/mod.rs:127-146`): a tool’s “ID card” is three things—the name `"shell"`; a manual for the model, which specifically writes “commands run under such-and-such a shell (overridable with `GOOSE_SHELL`)…… each stream of output at most 2000 lines, longer stored in a temp file,” so the model has the right expectation of behavior; and a JSON Schema parameter definition auto-generated by `schemars` from the Rust struct `ShellParams`. The model fills parameters by schema (e.g. `{"command": "ls -la"}`); goose deserializes with `serde_json::from_value` into a strongly typed struct then executes (`parse_args` at `developer/mod.rs:92-99`)—illegal parameters do not crash; the parse error is fed back to the model as a tool result so it can correct itself (`developer/mod.rs:199-205`). This “errors are also conversation content” design runs through every tool.

### Permissions: four modes + five-level verdict

goose’s “throttle and brake” is `GooseMode` (`crates/goose-provider-types/src/goose_mode.rs:22-31`):

```rust
pub enum GooseMode {
    Auto,          // 自动批准所有工具调用（默认）
    Approve,       // 每次工具调用都问用户
    SmartApprove,  // 只问"敏感"的工具调用
    Chat,          // 纯聊天，禁用工具
}
```

Every tool request, before execution, must pass `PermissionInspector` (`crates/goose/src/permission/permission_inspector.rs:144-196`); the verdict order is like airport security:

```mermaid
flowchart TD
    R[工具请求] --> M{GooseMode?}
    M -->|Auto| A1[直接放行]
    M -->|Chat| A2[跳过: 回复"工具不可用"]
    M -->|Approve / SmartApprove| U{用户对该工具有规则?}
    U -->|AlwaysAllow| A3[放行]
    U -->|NeverAllow| A4[拒绝]
    U -->|AskBefore| A5[弹窗问用户]
    U -->|无规则| RO{"SmartApprove 且工具带<br/>只读注解 readOnlyHint?"}
    RO -->|是| A6[放行]
    RO -->|否| EX{"是 manage_extensions<br/>(装扩展)?"}
    EX -->|是| A7[强制问用户<br/>"装扩展涉及安全"]
    EX -->|否| LLM{"SmartApprove?"}
    LLM -->|是| J[LLM 裁判判断只读/写<br/>permission_judge]
    LLM -->|否| A8[问用户]
    J -->|判定只读| A9[放行并缓存]
    J -->|判定会写| A10[问用户并缓存 AskBefore]
```

A few details worth saying:

- **SmartApprove relies on two information sources**: the MCP tool’s own `readOnlyHint` annotation (`permission_inspector.rs:172-176`), and an **LLM judge** (`permission/permission_judge.rs`, prompt in `prompts/permission_judge.md`)—let a (usually smaller) model decide “is this call read-only.” Verdicts are cached (`cache_non_readonly_decision`, `permission_inspector.rs:22-34`).
- **Installing an extension always asks**: the `manage_extensions` tool is treated specially (`permission_inspector.rs:177-181`), because installing an extension = introducing new code, highest risk.
- **When there is no verdict basis, default to asking the user** (`permission_inspector.rs:191-194`)—better annoying than allowing.
- Above permissions there is also a **security inspector** that can veto already-approved calls: under `crates/goose/src/security/` there is prompt-injection scanning (`scanner.rs`, `patterns.rs`), egress detection (`egress_inspector.rs`), etc.; their results override the permission conclusion as an “override” (`permission_inspector.rs:116-128`). There is also a **hooks** system, allowing external plugins to intercept at 10 event points including `PreToolUse`, `BeforeShellExecution` (`hooks/mod.rs:67-76`).

### Shell-execution safety design

The developer extension’s `shell` tool has several layers of consideration (`developer/shell.rs`, `developer/mod.rs:127-146`):

1. **Default is not a login shell**: auto-detect prefers `bash`, falls back to `sh` (`shell.rs:141-145`); the comment explains LLMs often write syntactically fancy commands, and login-shell differences are too large and error-prone; to use your own shell override with `GOOSE_SHELL` (`shell.rs:87-96`). Windows defaults to `cmd`.
2. **Output rate limit**: stdout/stderr returned separately, each stream at most 2000 lines, over-long written to a temp file (`developer/mod.rs:133-134`)—prevents one `cat` of a large file from blowing the context window.
3. The tool annotation marks it non-read-only (`developer/mod.rs:140-146`), so in SmartApprove mode it triggers approval; there is also a `BeforeShellExecution` hook for a second intercept.

---

## 8. Context compaction and memory: the goose’s “goldfish brain” self-rescue

Even a large model context window fills. goose has three layers of response:

### Layer one: active compaction

`check_if_compaction_needed()` at `crates/goose/src/context_mgmt/mod.rs:213` triggers when token usage exceeds a threshold (default `DEFAULT_COMPACTION_THRESHOLD = 0.8`, i.e. 80% of the usable window, `mod.rs:24`). Compaction is executed by `compact_messages()` (`mod.rs:76`); the method is quite “dignified”:

- **Does not delete history**; instead marks old messages “visible to the user, invisible to the model” (`mod.rs:139-149` uses `with_agent_invisible()`)—you can still flip through the full record in the UI, but the model only sees the summary;
- Lets the LLM generate a summary message, marked “model-visible only” (`mod.rs:151`);
- Appends a “bridging line,” telling the model “your context was compacted, don’t make a fuss, keep working” (`mod.rs:34-47`, three wordings for three scenes: ordinary conversation, in a tool loop, user-manual `/compact`);
- On non-manual compaction **keep the most recent pure-text user message** (`mod.rs:101-132`), so the model remembers “what the user actually wanted last.”

Chapter 6 said: when the provider reports `ContextLengthExceeded` directly, there is also a fallback compaction once.

### Layer two: tool-call pair summarization

In a long session what occupies space is often tool output (e.g. thousands of lines of test logs). `maybe_summarize_tool_pairs()` (`context_mgmt/mod.rs:624`) will in the background hand “tool request + response” pairs to the LLM for summary, 10 pairs per batch (`mod.rs:26`); default can be turned off with `GOOSE_TOOL_PAIR_SUMMARIZATION` (`mod.rs:28-32`). The threshold is computed by `compute_tool_call_cutoff` (`mod.rs:498`); the agent loop starts it once per round at `agent.rs:2051-2062` (done only once for the whole turn, see the `tool_pair_summarization_done` flag).

**A contrast example to feel the two layers’ division of labor**: the agent ran shell 30 times, each outputting 500 lines of test logs—overall tokens have not yet hit the 80% threshold, layer one does not trigger, but layer two finds “too many tool-call pairs,” summarizing early `shell` output as “tests 1-3: all failed for missing mock,” instantly saving tens of thousands of tokens; when the whole session (including post-summary content) continues to inflate to the threshold, layer one finally acts, folding the whole history into one summary. The former is “tidy the desk as you go”; the latter is “pack to move house.”

### Layer three: memory

Compaction is “short-term memory shrinking”; true **long-term memory** is provided by two extensions:

- **memory extension**: `remember_memory` stores facts by “category + tags” locally (`crates/goose-mcp/src/memory/mod.rs:335-452`); `retrieve_memories` retrieves by category. E.g. let goose remember “the deploy server is at 10.0.0.8”; the next session can look it up.
- **chatrecall extension**: search historical sessions, load old-session summaries—equivalent to “flip through previous chat logs.”

The session itself lives in a local **SQLite** database `sessions.db` (`crates/goose/src/session/session_manager.rs:28`); old-version `.jsonl` session files have migration logic (`session/legacy.rs`). Session names are also auto-generated by an LLM (`session/session_naming.rs`, prompt `prompts/session_name.md`, agent loop `agent.rs:1892-1907` generates asynchronously).

```mermaid
flowchart LR
    subgraph 写入路径
        A[每轮对话] --> B["token 用量 ≥ 80%?"]
        B -->|是| C["compact_messages<br/>旧消息→隐形, LLM 摘要→可见"]
        A --> D["工具输出太多?"]
        D -->|是| E["tool pair 摘要<br/>每批 10 对"]
    end
    subgraph 长期记忆
        F["memory 扩展<br/>分类+标签"]
        G["chatrecall 扩展<br/>搜索旧会话"]
        H["sessions.db<br/>(SQLite 完整历史)"]
    end
    C --> H
    F --> H
    G --> H
```

---

## 9. Sub-agents and multi-agent: a flock collaborating

goose’s multi-agent capability mainly hides in two platform extensions:

### summon extension: `delegate` spawns a sub-agent

`summon` provides two tools (`platform_extensions/summon.rs:576, 649`):

- `load`: load a “knowledge source” into the current context—can be a file, a URL, or a **sub-recipe**;
- `delegate`: **delegate the task to an independent sub-agent**. The sub-agent has its own session, its own context window; when done it brings back only the conclusion text (`run_subagent_task` at `subagent_handler.rs:48-63`, finally `extract_response_text` extracts text).

Available sources for a sub-agent include “the current recipe’s sub_recipes” (`summon.rs:1148`). The sub-agent has its own system-prompt template (`prompts/subagent_system.md`), and can also have an independent model config (`subagent_task_config.rs:15`).

### sub-recipe: “sub-task cards” inside a recipe

A Recipe (next chapter) can declare `sub_recipes` (`crates/goose/src/recipe/mod.rs:82`); each sub-recipe has `name`, `path`, `values` (pass parameters), `sequential_when_repeated` (whether to queue serially when called repeatedly, `mod.rs:119-128`). The main agent calls them via summon’s `delegate`, realizing “head chef directing sous-chefs.”

### orchestrator extension: multi-session master control

`orchestrator` (off by default, hidden from the UI, `platform_extensions/mod.rs:164-176`) is prepared for “agent managing agents”: `list_sessions` to see which agent sessions exist, `start_agent` to start a new session in a specified directory, `send_message` to send a message, `interrupt_agent` to interrupt (`orchestrator.rs:593-619`). CLI `goose review` (code review) uses this kind of orchestration to check multiple dimensions in parallel.

```mermaid
sequenceDiagram
    participant M as 主 Agent
    participant S as summon::delegate
    participant A as 子 Agent (新会话)
    M->>S: delegate { source: "代码审查配方", task: "审查 src/auth" }
    S->>A: run_subagent_task<br/>(subagent_handler.rs:48)
    Note over A: 独立上下文/独立工具执行<br/>跑自己的 reply 主循环
    A-->>S: 最终文本结论
    S-->>M: 作为工具结果返回
    Note over M: 主 Agent 上下文里只有结论<br/>没有子代理的中间过程
```

The benefit of this design is **context isolation**: the noise of a sub-agent flipping dozens of files does not pollute the main session; the main agent only sees “the report.” The cost is information loss—pits the sub-agent stepped in, the main agent does not know.

**Contrast another usage to feel the two routes**: if you only want the agent to “understand this directory first then decide what to do,” use `summon::load` to pull knowledge (file content, URL body, sub-recipe notes) into the **current** context, and the main agent digests it personally; if the task boundary is clear and the process is destined to be long (“add a license header to these 50 files one by one”), `delegate` to a sub-agent is more appropriate—the main session only receives one sentence “all done, 3 files have conflicts needing a human.” The former is “put the materials on a colleague’s desk”; the latter is “outsource the whole block of work.”

---

## 10. Ecosystem: MCP, recipes, and command inventory

### MCP is goose’s “USB port”

Almost all of goose’s external capability attaches through **Model Context Protocol** (Rust side uses `rmcp` 1.4, `Cargo.toml:23`). For the user, “install an extension” is registering an MCP server: a command (stdio), a URL (streamable HTTP), or a built-in name. For the model, there is also a meta-capability—`search_available_extensions` and `manage_extensions` tools let it **discover and install extensions itself** (`ext_manager.rs:269-344`); after install the tool list refreshes immediately (`tools_updated` in the agent loop at `agent.rs:2338-2343` triggers rebuilding the prompt). Of course, as Chapter 7 said, installing an extension forcibly needs user approval, and new extensions go through a malware check (`agents/extension_malware_check.rs`).

### Recipe: a shareable “workflow playbook”

Recipe is one of goose’s most recognizable concepts: a **YAML or JSON file** (`recipe/mod.rs:25`) describing how a complete task should run. Structure (`recipe/mod.rs:41-86`):

| Field | Role |
|---|---|
| `title` / `description` | name and description (required) |
| `instructions` | system-level instructions for the model (fill at least one of this and `prompt`) |
| `prompt` | session opening line |
| `extensions` | which extensions this recipe needs |
| `settings` | specify provider/model/temperature/max turns (`mod.rs:98-110`) |
| `parameters` | variables the user fills (for template rendering) |
| `activities` | “suggested action” pills shown on desktop |
| `response` | **JSON Schema**: the structure the final output must match (forced validation with the final_output tool, `final_output_tool.rs`) |
| `sub_recipes` | sub-recipes (Chapter 9) |
| `retry` | auto-retry on failure (agent loop `handle_retry_logic` at `agent.rs:2765`) |

Recipes support template variables (`template_recipe.rs`), deep-link sharing (`goose recipe deeplink`, `recipe_deeplink.rs`), security scanning (repo-root `recipe-scanner/`), plus official examples (`workflow_recipes/`, `examples/`, `CONTRIBUTING_RECIPES.md`). `goose run --recipe xxx.yaml` can execute unattended—`goose-self-test.yaml` is the project’s own smoke test (`AGENTS.md:71`).

**An imagined minimal recipe**, to feel how the fields plate together:

```yaml
version: 1.0.0
title: 每日站会纪要
description: 汇总昨天的 git 提交，生成站会发言稿
instructions: 你是团队助理，用 git log 收集昨天提交，按人分组写成中文纪要。
prompt: 请生成今天的站会纪要。
extensions:
  - type: builtin
    name: developer        # 需要 shell 跑 git log
settings:
  goose_model: claude-sonnet-4-5
  max_turns: 20
response:
  json_schema:             # final_output 工具会强制模型按此结构收尾
    type: object
    properties:
      summary: { type: string }
      blockers: { type: array, items: { type: string } }
retry:
  max_retries: 3
```

Send this file to a colleague; they `goose run --recipe standup.yaml` and reproduce the identical flow—that is the pain recipe wants to solve: **an agent’s “play” can for the first time be version-managed, reviewed, and shared like a script**.

### CLI command inventory (`cli.rs`’s `Command` enum)

`configure` (initial config), `session` (sessions: list/remove/export/diagnostics…), `run` (execute an instruction file/recipe, can be unattended), `recipe` (validate/deeplink/list), `schedule` (cron tasks add/list/remove/run-now), `mcp` (run a bundled MCP server alone), `acp` (stdio ACP server), `serve` (HTTP/WebSocket ACP server), `skills`, `plugin` (install plugins from git), `project` (project-directory management), `term` (terminal integration), `tui` (experimental terminal UI), `local-models` (local model management), `review` (code review), `gateway` (remote gateway), `update`, `doctor` (self-check), `info`, `completion` (shell completion), `validate-extensions`.

### Custom extensions and skills

Besides writing an MCP server, there are several lightweight paths: **Frontend tools** (Desktop uses TypeScript to give the model capabilities), **InlinePython extensions**, **plugin** (extension packages in git-repo form, `commands/plugin.rs`), and **skills**—put `SKILL.md` in `~/.agents/skills` or the project’s `.agents/skills` (`crates/goose/src/skills/mod.rs:38-45`); the model uses the `load_skill` tool to load detailed instructions on demand, avoiding stuffing all knowledge into the system prompt at once.

```mermaid
flowchart TD
    subgraph 扩展来源
        A["MCP 服务器<br/>(stdio/HTTP)"]
        B["捆绑扩展<br/>goose-mcp"]
        C["平台扩展<br/>Rust 进程内"]
        D["Frontend/InlinePython"]
        E["Skills 技能文件<br/>SKILL.md"]
        F["Recipes 配方<br/>YAML/JSON"]
    end
    A & B & C & D --> EM["ExtensionManager<br/>统一孵化/管理"]
    EM --> T["工具列表<br/>ext__tool 命名"]
    E --> T2["load_skill 按需加载"]
    F --> R["会话配置:<br/>指令+扩展+参数+重试"]
    T & T2 & R --> AG["Agent 主循环"]
```

---

## 11. Features: models, auth, and distinctive capabilities

### Provider lineup

This is goose’s most “luxurious” part. The registry is `crates/goose/src/providers/init.rs:62-150`, in three (plus one) classes:

1. **Hand-written providers**: Anthropic, OpenAI, Google, Ollama, OpenRouter, Azure OpenAI, AWS Bedrock, GCP Vertex, Databricks (two generations), GitHub Copilot, HuggingFace, LiteLLM, NanoGPT, SageMaker TGI, Snowflake, Tetrate, xAI (API key and OAuth two kinds), Avian;
2. **“Subscription reuse” type**: `ClaudeCodeProvider` (reuse a Claude subscription), `ChatGptCodexProvider` / `CodexProvider` (reuse ChatGPT/Codex subscriptions), `GeminiCliProvider` (reuse Gemini CLI login), `KimiCodeProvider`, `CursorAgentProvider`—these piggyback your existing account via CLI or OAuth, no separate API purchase;
3. **ACP providers**: Amp, Claude, Codex, Copilot, Pi (the `*_acp` series at `init.rs:62-93`), i.e. use another agent as the model backend;
4. **Declarative providers**: 37 JSON definitions (`crates/goose-providers/src/declarative/definitions/`)—DeepSeek, Groq, Mistral, Moonshot, zhipu, alibaba, Cerebras, Fireworks, LM Studio, Together and other OpenAI-compatible endpoints; adding a new provider is adding one JSON file.

**Local models** are also supported: Ollama over HTTP API; the `goose-local-inference` crate ships llama.cpp (`llama-cpp-2`, `Cargo.toml:104`) and candle runtimes, with the `goose local-models` (alias `goose lm`) command to search HuggingFace and download GGUF/MLX models (from `cli.rs:1201`). Auth covers API key, OAuth device-code flow (`providers/oauth_device_flow.rs`), system keychain storage (`keyring` crate), etc.

### Model selection and lead/worker

Models are specified via the `/model` slash command, `GOOSE_PROVIDER`/`GOOSE_MODEL` config, or a recipe’s `settings`. **Need to be honest**: in the current commit’s code, I **did not find** the lead/worker dual-model mode early goose versions had (`GOOSE_LEAD_MODEL`-related code no longer exists)—nearby capability now shows up as independent sub-agent model config (`subagent_task_config.rs`) and recipe settings.

### Distinctive features

- **Scheduled tasks (schedules)**: `crates/goose/src/scheduler.rs` implements cron scheduling on `tokio-cron-scheduler`; `goose schedule add "0 9 * * *" --recipe daily.yaml` lets goose auto-run a recipe at 9 every day; `schedule sessions` can also look up sessions a scheduled task has produced.
- **Session management**: SQLite storage, LLM auto-naming, `session export`, resume/fork, `diagnostics` diagnostic packing.
- **Plan mode**: `/plan` enter, `/endplan` exit; the CLI first lets the model produce a plan and classifies “is this a plan or a clarifying question” (`session/mod.rs:236-270`).
- **goal / grind**: `/goal` sets a goal; every time the agent wants to clock out it is “invisibly nudged” until the goal is met (`agent.rs:2720-2755`); `grind` is the harder version—“if it’s not done, keep grinding.”
- **Apps**: the model can generate sandboxed HTML/JS mini-apps, run in an independent window (`apps.rs`)—goose’s “Artifacts.”
- **Hooks and telemetry**: 10 lifecycle hook events for plugins to intervene; PostHog telemetry is explicit opt-in; OpenTelemetry can export traces/metrics.

**String several distinctive features into a real scene**: Monday 8:55, the scheduler fires the “tidy last week’s issues” recipe by cron; goose runs headless and writes the result into a new session; you open desktop at 9, see it in the session list (the name is LLM-auto-generated “last week’s issue summary”), then use `/goal` to set “fix the highest-priority bug today”; the agent starts working—every time it thinks it can clock out, the goal nudge pulls it back; mid-way you feel a change is dangerous, type `/mode smart_approve` to tighten the throttle, and subsequent file writes pop a question first. This whole flow never left goose; that is the everyday the “schedule + session + goal + permission” combo wants to cover.

```mermaid
mindmap
  root((goose 特性))
    模型
      15+ 手写 provider
      37 个声明式 JSON provider
      订阅复用 Claude/ChatGPT/Gemini
      本地 Ollama / llama.cpp
    自动化
      cron 定时任务
      recipe 无人值守
      钩子 hooks
    会话
      SQLite 存储
      LLM 自动命名
      恢复/分叉/导出
    交互
      计划模式
      goal/grind 目标
      Apps 沙盒应用
      图表 autovisualiser
```

---

## 12. Summary: design philosophy and trade-offs

After walking the whole zoo, several of goose’s “distinctive ideas” can be distilled:

1. **Every capability is an extension, including kernel capabilities**. Reading/writing files, todo lists, code analysis—these “standards” are also extensions in goose, walking the same pipeline as third-party MCP servers (permissions, approval, tool naming, prompt injection). But note: `Builtin` (in-process pipe) and `Platform` (pure Rust written directly in the agent process) two extension shapes **do not walk the MCP protocol**—they are “cheat” shapes goose invented to claw back the performance cost of “one process per MCP extension.”
2. **The engine *is* the server; the GUI is only a client**. `goose serve` turns the agent into a local ACP service; desktop, Zed, any third party can attach. This lifts “agent” from “a terminal program” to “a kind of on-machine infrastructure”—the goose’s most far-sighted bet.
3. **Engineered for prompt cache**. Hour-precision timestamps, stable tool ordering, static system prompt + dynamic MOIM sticky-note separation—everywhere you see an obsession with prompt-cache hits, because a cache hit is real gold-and-silver latency and cost.
4. **Recipe as a shareable “workflow unit.”** Pack “instructions + extensions + parameters + output schema + retry” into one YAML; with deep links and cron scheduling, goose tries to let agent workflows circulate like npm packages.
5. **Permissions use “modes” rather than a “rule table” as the main line.** Auto/Approve/SmartApprove/Chat four grades, simple and understandable, then supplemented by read-only annotations, an LLM judge, a security scanner, hooks as a backstop. The trade-off is clear: ordinary users are not scared off by complex rule syntax; advanced needs go to extensions and hooks.

**Main trade-offs**: Rust brings single-file deploy and performance, but async Rust’s learning curve screens out some potential contributors; the MCP process model buys ecosystem compatibility, in exchange for extension startup latency and a larger failure surface; SmartApprove’s LLM judge is clever but introduces a “use a model to judge a model” trust problem; extensions dynamically starting and stopping is flexible, but the suggested caps `MAX_EXTENSIONS = 5` / `MAX_TOOLS = 50` (`prompt_manager.rs:19-20`) also admit a reality—too many tools, and even a strong model’s eyes glaze over.

If one sentence summarizes this goose: **it designed itself as a local agent engine that is “model-neutral, protocol-open, and can be ridden by anything”**—and that, with Claude Code’s route of “polish the terminal experience to an extreme around a single model,” happens to constitute two representative answers in the open-source agent world.

```mermaid
flowchart LR
    subgraph ClaudeCode["Claude Code 路线"]
        X1[单一强模型] --> X2[极致终端体验]
    end
    subgraph Goose["goose 路线"]
        Y1[模型中立] --> Y3["本地 agent 引擎"]
        Y2[MCP 开放生态] --> Y3
        Y4[Recipe 工作流] --> Y3
        Y5[GUI/CLI/API 三端] --> Y3
    end
    style Goose fill:#e6f7ff,stroke:#1890ff
```

---

*(End of lecture. All source citations are based on commit d17d65f; to check, you can run `git show <commit>:<file path>` in the repo directory.)*
