# goose 源码分析：一只会用工具的"本地鹅"

> **分析对象**：`aaif-goose/goose`（Block 公司 goose 的开源分支，现归属 Linux 基金会 Agentic AI Foundation）
> **分析基于的 commit**：`d17d65f2f30876e391395e1e8b5f2666320588c6`（2026-07-23，`feat: support latest Gemini models (#10630)`）
> **分析日期**：2026-07-24
> **代码规模**：Rust 约 463 个 `.rs` 文件（Cargo workspace，12 个 crate）+ TypeScript 228 个 `.ts` / 361 个 `.tsx`（Electron 桌面端等），工作区版本 `1.44.0`（`Cargo.toml:11`）

这份文档面向计算机初学者。我们会像逛动物园一样逛这个代码库：先看地图（架构），再看每只动物的习性（模块），最后总结这只"鹅"（goose 直译为"鹅"）的设计哲学。文中所有 `文件:行号` 形式的引用都真实存在于上述 commit 中，读者可以按图索骥。

---

## 1. 项目概览：goose 是什么？

goose 是一个**跑在你自己电脑上的通用 AI agent**（智能体）。README 里写得很直白：

> "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`）

几个关键事实：

- **谁在做**：最初由 Block（Square/Cash App 的母公司）开发，作者邮箱还留在 `Cargo.toml:13`（`ai-oss-tools@block.xyz`）。现在是 Linux 基金会旗下 **Agentic AI Foundation (AAIF)** 的项目（`README.md:29`），治理方式写在 `GOVERNANCE.md` 里——轻量级技术治理，核心价值观是 **Open / Flexible / Choice**（开放、灵活、可选择）。
- **三种形态**：macOS/Linux/Windows 的**桌面应用**（Electron）、完整的**命令行工具 CLI**（Rust）、以及可嵌入别处的 **API**（`README.md:25`）。
- **技术栈**：核心逻辑全部用 **Rust** 写成（一个 Cargo workspace，12 个 crate）；桌面端是 **Electron + React + TypeScript**；另外还有一个用 Ink（React 渲染到终端字符网格）写的实验性终端 UI（`ui/text`，见 `AGENTS.md:88`）。
- **模型中立**：支持 15+ 提供商——Anthropic、OpenAI、Google、Ollama、OpenRouter、Azure、Bedrock 等（`README.md:27`），还能通过 **ACP**（Agent Client Protocol）复用你已有的 Claude / ChatGPT / Gemini 订阅。
- **扩展靠 MCP**：通过开放标准 **Model Context Protocol (MCP)** 连接 70+ 扩展（`README.md:27`）。

**与 Claude Code 的一句话对比**：Claude Code 是 Anthropic 官方的、绑定 Claude 模型的 TypeScript 终端 agent；goose 则是**模型中立、用 Rust 写的开源 agent**，同一套核心既驱动 CLI 也驱动桌面 GUI，工具生态完全建立在 MCP 开放协议上。两者都会读 `AGENTS.md`/`CLAUDE.md` 这类项目提示文件，都有斜杠命令、权限模式和上下文压缩，但 goose 把"工具"彻底外置成了"扩展（extension）"，并把工作流做成了可分享的"配方（recipe）"。

```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. 全景架构：一套 Rust 核心，三种外壳

打开仓库根目录，最重要的两个文件夹是 `crates/`（Rust 源码）和 `ui/`（TypeScript 源码）。`Cargo.toml:2-6` 声明 workspace 成员为 `crates/*`，共 12 个 crate：

| crate | 职责 |
|---|---|
| `goose` | **心脏**：Agent 主循环、扩展管理、提示词、压缩、权限、配方、调度器 |
| `goose-cli` | CLI 外壳：命令解析、交互式会话、渲染 |
| `goose-mcp` | 随鹅捆绑的 MCP 服务器（memory、computercontroller 等） |
| `goose-providers` / `goose-provider-types` | 模型提供商实现与共享类型 |
| `goose-sdk` / `goose-sdk-types` | 给外部程序嵌入用的 SDK |
| `goose-acp-macros` | ACP 协议的过程宏 |
| `goose-local-inference` / `goose-download-manager` | 本地推理（llama.cpp/candle）与模型下载 |
| `goose-test` / `goose-test-support` | 测试工具 |

`ui/` 下则有 `desktop`（Electron 桌面应用）、`text`（Ink 终端 UI）、`goose-binary`、`sdk` 等。

**关键设计：CLI 和 Desktop 共享同一个 `goose` 核心 crate，但方式不同。**

- CLI 直接链接 `goose` 库，进程内调用 `Agent::reply()`。
- Desktop 不直接链接 Rust 库，而是** spawn（孵化）一个 `goose serve` 子进程**——这是一个 ACP over HTTP/WebSocket 的服务器（`crates/goose-cli/src/cli.rs:844`，"Start ACP server over HTTP and WebSocket"）。Electron 主进程里的 `ui/desktop/src/gooseServe.ts:320` 负责启动它，前端再通过 WebSocket 用 ACP 协议与之对话。

换句话说：**goose 把"agent 引擎"做成了本地服务器，桌面 GUI 只是它的一个客户端**。这和 Claude Code 的"SDK + 终端 UI"思路神似，但 goose 走得更彻底——连自家 GUI 都通过协议通信，这意味着任何第三方程序都能用同一协议"骑"上这只鹅。

```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. 启动流程：从二进制到对话界面

### CLI 端

入口极小：`crates/goose-cli/src/main.rs:34` 的 `main()`。它做的事可以概括为"搭台子，唱大戏"：

```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 异步运行时
    })?;
```

为什么要单开线程？因为 agent 递归处理消息时调用栈可能很深，默认线程栈（通常 2MB）不够用。`run()` 里初始化日志后调用 `cli()`（`crates/goose-cli/src/cli.rs:2205`），后者用 **clap** 解析命令行，得到一个巨大的 `Command` 枚举（`cli.rs:803` 起）——`session`、`run`、`recipe`、`schedule`、`mcp`、`acp`、`serve`、`configure`、`doctor`……

如果你直接敲 `goose` 不带任何子命令，会走到 `cli.rs:2378` 的 `None => handle_default_session().await`，即 `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` 会创建 `Agent`、加载配置里启用的扩展（每个扩展可能是一个子进程），最后 `session.interactive()` 进入下一章讲的输入循环。

### Desktop 端

桌面端入口是 Electron 主进程 `ui/desktop/src/main.ts`。它做窗口管理、菜单（含中文菜单翻译，`main.ts:73` 起）、自动更新等，然后通过 `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
```

值得一提：Desktop 与后端之间要求 `GOOSE_SERVER__SECRET_KEY` 鉴权（`gooseServe.ts:336`），防止本机其他程序随意接入这个能执行 shell 命令的服务器——这是安全设计的一部分。

**CLI 启动时还有一条隐藏分支值得知道**：`handle_default_session` 的第一件事是检查配置文件是否存在（`cli.rs:2166-2168`），不存在就直接进入交互式配置向导 `handle_configure()`——也就是说新用户第一次敲 `goose`，看到的不是报错而是"欢迎使用，请选择你的模型提供商"的引导界面。这个"先问诊再开药"的顺序，避免了新手面对一堆环境变量不知所措。

---

## 4. 输入捕获与分流：斜杠命令是"本地快捷指令"

### CLI：rustyline 逐行读取

CLI 的输入由 **rustyline**（一个 readline 风格的 Rust 库）负责，入口是 `crates/goose-cli/src/session/input.rs:117` 的 `get_input()`。读入一行后，先做一个关键分流（`input.rs:198-218`）：

1. 如果**不是** `/` 开头 → 包成 `InputResult::Message`，直接送给 agent；
2. 如果是 `/` 开头 → 交给 `handle_slash_command()`（`input.rs:221`）匹配内置命令。

所有可能的结果定义在 `input.rs:15-36` 的 `InputResult` 枚举里：

```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` 也有别名机制：`COMPACT_TRIGGERS`（`crates/goose/src/agents/execute_commands.rs:11`）。一个很人性化的细节是 **Ctrl+C 的双重含义**（`input.rs:60-87`）：当前行有文字时按 Ctrl+C 只是清空输入行；空行时按第一次显示"再按一次退出"提示，按第二次才真正退出——避免了手滑丢会话。

**举一个分流的例子**：你输入 `/mode smart_approve`，它不会发给模型，而是变成 `InputResult::GooseMode("smart_approve")`，由 CLI 本地切换权限模式；而你输入 `帮我把 README 翻译成中文`，则作为 `Message` 进入 agent 主循环。

**再举两个覆盖不同情形的例子**：其一，输入一个不存在的命令如 `/dance`，`handle_slash_command` 匹配不到会返回 `None`，于是整个字符串原样发给模型——模型大概率会回答"我不知道 /dance 是什么"，而不是 CLI 报错，这让未来的命令可以靠模型兜底。其二，粘贴一大段带换行的报错日志时，`paste.rs` 的粘贴感知输入（`read_paste_aware_input`，`input.rs:2-3` 引入）会把整块粘贴内容当作一次输入而不是逐行触发，避免日志里的空行被误判成"发送"。

### Desktop / ACP 端

桌面端的斜杠命令清单由 `crates/goose/src/slash_commands/slash_command.rs:20` 的 `list_acp_commands()` 提供，它合并三个来源并去重（`slash_command.rs:30-53`），优先级是 **内置 > 配方 > 技能**：

- **内置命令**（`execute_commands.rs:21-56`）：`prompts`、`prompt`、`compact`、`clear`、`skills`、`doctor`、`goal`、`grind`、`status`；
- **配方命令**：当前加载的 recipe 暴露的快捷方式；
- **技能命令**：磁盘上发现的 SKILL.md 技能。

```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. 上下文组装：系统提示词是怎么"拼"出来的

每次向模型发请求前，goose 都要拼一份**系统提示词（system prompt）**。总装车间是 `crates/goose/src/agents/prompt_manager.rs`，模板是 `crates/goose/src/prompts/system.md`（用 minijinja 风格的模板语法）。

### 模板里有什么

`system.md` 开头固定是自我介绍（`system.md:1`）：

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

接着按条件插入：MOIM 说明块、**扩展清单**（`{% for extension in extensions %}`，每个扩展的名字、是否支持资源、使用说明）、扩展数量超限建议（超过 5 个扩展或 50 个工具时提醒用户精简，`prompt_manager.rs:19-20`）、以及"用 Markdown 回复"等输出规范。

组装入口是 `prompt_manager.rs:117` 的 `build()`，它收集一个 `SystemPromptContext`（`prompt_manager.rs:36-49`）：扩展信息、当前时间、goose 模式、是否自治、是否启用子代理等。注意两个为**提示词缓存（prompt cache）**而生的细节：

- 时间戳**只精确到小时**（`prompt_manager.rs:207-209`：`%Y-%m-%d %H:00`）——这样同一小时内多次请求的系统提示词完全一致，可以命中模型提供商的缓存，省钱省时；
- 扩展信息**按名字排序**（`prompt_manager.rs:129`），注释明说"Stable tool ordering is important for multi session prompt caching"。

### 提示文件：.goosehints 与 AGENTS.md

goose 的项目级"悄悄话"文件规则在 `crates/goose/src/hints/load_hints.rs`：

- 默认收集两个文件名：**`.goosehints`** 和 **`AGENTS.md`**（`load_hints.rs:10-11`）；可用配置项 `CONTEXT_FILE_NAMES` 改成任意文件名（`load_hints.rs:13-24`）——比如让它去读 `CLAUDE.md`，测试里就有这个用例（`load_hints.rs:458-468`）。
- **全局层**：配置目录下的同名文件，外加 `~/.agents/AGENTS.md`（`load_hints.rs:233-242`）。
- **项目层**：从 git 根目录一路向下到当前工作目录，每一级的提示文件都会被收集（`load_hints.rs:263-285`）；如果不是 git 仓库，就只看当前目录（测试 `load_hints.rs:505-535` 验证了这点）。
- **支持 `@文件` 导入**：提示文件里写 `@docs/api.md` 会把该文件内容内联进来，但**导入边界是 git 根目录**——`@../../../forbidden.md` 这种越界引用会被拒绝（测试 `load_hints.rs:635-688`），而且被 `.gitignore` 忽略的文件（如 `secret.env`）即使被 `@` 也不会读进来（测试 `load_hints.rs:856-879`）。这是防提示注入泄漏机密的重要一环。
- **子目录提示动态加载**：`SubdirectoryHintTracker`（`load_hints.rs:26-96`）会盯着工具调用的参数——如果 agent 用 `write` 写了 `nested/foo.rs` 或用 shell `cat nested/doc.md`，它就把 `nested/` 记为"待检查目录"，下一轮发现那里有 `.goosehints` 就自动补进系统提示（主循环里 `agent.rs:2666-2676` 负责在发现新提示时重建 prompt）。

收集到的 hints 以 `### Global Hints` / `### Project Hints` 小标题形式，追加到系统提示词的 `# Additional Instructions:` 段落（`prompt_manager.rs:193-198`）。

**举个层层叠加的例子**：你的 home 配置目录有一份全局 `.goosehints`（写着"回复用中文"），git 仓库根目录有一份 `AGENTS.md`（写着"这是 Rust 项目，改完必须 `cargo clippy`"），你此刻在 `src/utils/` 子目录下工作且该目录还有一份 `.goosehints`（写着"本目录是性能敏感代码"）——最终系统提示里会同时出现这三段话，模型对全局偏好、项目规矩、局部注意事项一目了然。这和 Claude Code 收集 `CLAUDE.md` 的思路几乎一致，只是 goose 额外给了你自定义文件名的自由。

### 每轮还要塞一张"动态便签"：MOIM

系统提示词是"静态"的（为了缓存）；每轮变化的信息走另一条路——`crates/goose/src/agents/moim.rs` 的 `inject_moim()`（主循环 `agent.rs:2026-2032` 每轮调用）。它把当前时间、工作目录、token 用量/压缩状态、**剩余轮次预算（turn budget）**、以及各扩展提供的上下文，拼成一个 `<turn_context>` 文本块，**插入到最后一条用户消息里、工具响应之前**（`moim.rs:97-111`）。系统提示里会提前告知模型"这个块是操作上下文，不是用户请求"（`moim.rs:11-27`），并教它"预算快用完时少探索、快收尾"。上下文窗口小于 32k token 的模型直接跳过 MOIM（`moim.rs:9`）——小窗口经不起这种额外开销。

```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. Agent 主循环（心脏）：`reply_internal` 的一口气

整个 goose 最重要的一段代码在 `crates/goose/src/agents/agent.rs:1846` 的 `reply_internal()`。对外入口是 `agent.rs:1549` 的 `reply()`（处理 elicitation 响应、追加用户消息等前置工作后调进来）。主循环本体在 **`agent.rs:1948`** 的 `loop { ... }`。

先给一个"心跳"全景，再逐拍解释：

```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
```

逐拍解释：

1. **取消检查**（`agent.rs:1949`）：每轮开头先看 `CancellationToken`——用户按 ESC/Ctrl+C 中断时就靠它刹车。工具执行期间同样在 `tokio::select!` 里监听（`agent.rs:2276-2281`）。
2. **插队消息（steer）**：用户在 agent 干活时还能追加消息，`drain_pending_steers`（`agent.rs:1953-1973`）把它们排进会话，还会触发 `UserPromptSubmit` 钩子。
3. **轮次计数**：`turns_taken` 上限默认 `DEFAULT_MAX_TURNS = 1000`（`agent.rs:69`），也可用 `GOOSE_MAX_TURNS` 配置（`agent.rs:1931-1935`）。超限就输出 `MAX_TURNS_MESSAGE` 收尾。
4. **请求模型**：`stream_response_from_provider`（`reply_parts.rs:287`）先做两件预处理——把会话投影成"对 agent 可见"的消息并修复格式问题（`reply_parts.rs:298-301`），然后调 `provider.stream(model_config, system_prompt, messages, tools)`（`reply_parts.rs:322-330`）。如果模型不支持原生工具调用（toolshim 模式），就把工具描述塞进提示词、让模型输出 JSON 标记，再本地解析（`reply_parts.rs:269-278, 304-308`）——这是兼容小模型的"拐杖"。
5. **流式解析**：`while let Some(next) = stream.next().await`（`agent.rs:2079`）逐块读取。纯文本/思考内容直接 `yield AgentEvent::Message` 推给 UI；工具调用则走 `categorize_tools`（`agent.rs:2124`）分成两类：**frontend 工具**（由前端/Desktop 执行，比如"在屏幕上显示图表"）和**普通工具**。
6. **工具执行**：Chat 模式下所有工具请求直接回一个"已跳过"（`agent.rs:2191-2207`，`CHAT_MODE_TOOL_SKIPPED_RESPONSE`）；其他模式先过**工具检查器**（权限、安全扫描，见第 7 章），把请求分成 approved / needs_approval / denied 三堆（`agent.rs:2219-2232`），然后并发执行（`stream::select_all`，`agent.rs:2273`）。每个工具请求都会预先配好一条占位响应消息（`request_to_response_map`，`agent.rs:2172-2177`），执行完把结果填回去。
7. **追加与再请求**：工具请求消息和响应消息**成对**追加进会话（`agent.rs:2458-2524`），然后 `loop` 回到顶部再次请求模型——这就是经典的 "请求 → 工具 → 追加 → 再请求" agent 循环。注意 `agent.rs:2346-2456` 那一大段处理 **thinking（推理）内容**的代码：它把推理块挂到工具调用消息上而不是单独存，注释解释了原因——Anthropic 的签名思考块如果重复出现会被 API 以 400 拒绝，Gemini/Kimi/DeepSeek 又要求思考内容回显——这是被多家模型"夹击"出来的工程细节。
8. **终止条件**：一轮没调用任何工具时（`agent.rs:2697`），依次检查：final_output 工具（recipe 要求结构化输出时强制调用，`agent.rs:2705-2714`）→ 是否有插队消息 → **goal/grind 目标是否达成**（没达成就塞一条用户不可见、模型可见的催促消息，让 agent 继续干，`agent.rs:2720-2755`）→ recipe 重试逻辑 → 空响应兜底重试（最多 `MAX_EMPTY_TURN_RETRIES = 3` 次，`agent.rs:73, 2778-2785`）。都没有，循环才真正结束。
9. **上下文超限自愈**：如果 provider 报 `ContextLengthExceeded`（`agent.rs:2532`），先告诉用户"正在压缩"，调 `compact_messages` 压缩会话，然后**带着压缩后的会话 continue 循环**；压缩一次还不够（`compaction_attempts >= 2`）才放弃（`agent.rs:2538-2547`）。其他错误（余额不足 `CreditsExhausted`、拒答 `Refusal`、网络错误）各有专门分支，分别提示充值、建议开新会话、建议重发（`agent.rs:2592-2656`）。

**中断机制小结**：取消令牌贯穿三层——主循环开头、流式读取内、工具并发执行的 `select!` 里，保证任何时候按中断都能较快停下来。

**一个完整的"心跳"实例**：假设你说"帮我跑一下测试并修复失败的那个"。第一拍，模型返回文本"我先跑测试"加上一个 `shell {command: "cargo test"}` 工具调用；`categorize_tools` 把它归入普通工具，权限检查器放行后并发执行；stdout 被填进占位响应消息，与请求消息成对追加进会话。第二拍，模型读到测试输出，说"有一个断言失败，我来改"，又发出 `edit {path: "src/lib.rs", before: "...", after: "..."}`；执行成功后再次循环。第三拍，模型再跑一遍测试确认通过，然后只返回文本"修好了，失败原因是……"——这一轮没有任何工具调用（`no_tools_called = true`），所有终止条件检查通过，循环结束，控制权交还给你。整个过程中你在 UI 上看到的是流式逐字蹦出的文字和一张张工具调用卡片，背后就是这一个 `loop` 在呼吸。

---

## 7. 工具系统与权限：一切皆扩展

goose 里**没有内置工具，只有内置扩展（extension）**。所有能力——哪怕是读写文件——都是某个扩展通过 MCP 协议提供的工具（tool）。

### 扩展的六种形态

`crates/goose/src/agents/extension.rs:162` 的 `ExtensionConfig` 枚举定义了扩展的接入方式：

| 变体 | 含义 |
|---|---|
| `Stdio` | 启动一个本地子进程，通过标准输入输出讲 MCP（最常见） |
| `Sse` / `StreamableHttp` | 连接远程 MCP 服务器（SSE 已标记不受支持，`extension.rs:420`） |
| `Builtin` | goose 自带的 MCP 服务器，**进程内**通过内存管道讲 MCP（`goose-mcp` crate） |
| `Platform` | 平台扩展：直接用 Rust 写在 agent 进程里、能访问 agent 内部设施 |
| `Frontend` | 由前端（Desktop）提供的工具，调用要转发给 UI 执行 |
| `InlinePython` | 内联 Python 代码扩展 |

`ExtensionManager`（`agents/extension_manager.rs`）负责孵化它们：stdio 扩展用 `Command::new(cmd).spawn()`（`extension_manager.rs:431-432`），也支持 `uvx`（Python 系扩展）和 `docker` 包装（`extension_manager.rs:1035-1146`）。工具名的暴露规则是 **`扩展名__工具名`** 双下划线前缀（`extension_manager.rs:1425`），比如 `memory__remember_memory`；少数"一等公民"扩展（developer、analyze、summon、skills）配置为 `unprefixed_tools: true`，工具直接用裸名，比如 `shell`、`write`。

### 全部内置扩展与工具清单

**平台扩展**（写在 `crates/goose/src/agents/platform_extensions/`，注册表 `mod.rs:28-207`）：

| 扩展 | 默认启用 | 工具（前缀省略） | 功能 | 例子 |
|---|---|---|---|---|
| **developer** | ✅ | `shell` / `write` / `edit` / `tree` / `read_image` | 执行 shell、写/改文件、看目录树、读图片（`developer/mod.rs:101-172`） | `shell {command: "cargo test"}` |
| **analyze** | ✅ | `analyze` | 用 tree-sitter 分析代码结构、符号调用图（`analyze/mod.rs:221`） | `analyze {path: "src/"}` |
| **todo** | ✅ | `todo_write` | 维护任务清单（`todo.rs:117`） | 记下"1.改代码 2.跑测试" |
| **summon** | ✅ | `load` / `delegate` | 加载知识源；派生子代理（`summon.rs:576, 649`） | 第 9 章详述 |
| **skills** | ✅ | `load_skill` | 加载技能文件全文（`skills/client.rs:82`） | 加载"写 PPT"技能 |
| **extensionmanager** | ✅ | `search_available_extensions` / `manage_extensions` | 让 agent 自己搜索、启用、禁用扩展（`ext_manager.rs:269-344`） | "帮我打开浏览器扩展" |
| **apps** | ✅ | `list_apps` / `create_app` / `iterate_app` / `delete_app` | 生成沙盒 HTML 小应用（`apps.rs:521-538`） | "做个番茄钟" |
| **tom** (Top Of Mind) | ✅ | 无工具 | 用环境变量 `GOOSE_MOIM_MESSAGE_TEXT/FILE` 向每轮注入自定义上下文（`mod.rs:178-190`） | CI 里注入工单号 |
| **chatrecall** | ❌ | `chatrecall` | 搜索历史会话、加载摘要（`chatrecall.rs:257`） | "上次我们怎么配的 nginx?" |
| **summarize** | ❌ | `summarize` | 一次性"读文件+LLM 摘要"（`summarize.rs:74`） | 快速消化大目录 |
| **code execution**（feature: code-mode） | ❌ | `list_functions` / `get_function_details` / `execute_typescript` / `execute_bash` | 把工具调用变成写代码，省 token（`code_execution.rs:454-522`） | 用一段 TS 串起 5 个工具调用 |
| **orchestrator**（隐藏） | ❌ | `list_sessions` / `view_session` / `start_agent` / `send_message` / `interrupt_agent` | 管理多个 agent 会话（`orchestrator.rs:593-619`） | 第 9 章详述 |

**捆绑 MCP 扩展**（`crates/goose-mcp/`，注册表 `goose-mcp/src/lib.rs:57-64`）：

| 扩展 | 工具 | 功能 |
|---|---|---|
| **memory** | `remember_memory` / `retrieve_memories` / `remove_memory_category` / `remove_specific_memory`（`memory/mod.rs:335-432`） | 跨会话的长期记忆，按分类+标签存取 |
| **computercontroller** | `web_scrape` / `automation_script` / `computer_control` / `xlsx_tool` / `docx_tool` / `pdf_tool` / `cache`（`computercontroller/mod.rs`） | 网页抓取、系统自动化、办公文档处理 |
| **autovisualiser** | `render_sankey/radar/donut/treemap/chord/map/mermaid` / `show_chart`（`autovisualiser/mod.rs:892-1319`） | 生成图表并展示 |
| **tutorial** | `load_tutorial`（`tutorial/mod.rs:88`） | 新手教程 |
| **peekaboo**（仅 macOS，条件编译模块，**未进入 BUILTIN_EXTENSIONS 注册表**） | — | `lib.rs:18-19` 条件编译，但 `lib.rs:57-64` 的 `BUILTIN_EXTENSIONS` 仅注册 4 个（不含 peekaboo） |

**工具长什么样？以 `shell` 为例逐行看**（`developer/mod.rs:127-146`）：工具的"身份证"由三样东西组成——名字 `"shell"`；一段给模型看的说明书，里面特意写明"命令在某某 shell 下执行（可用 `GOOSE_SHELL` 覆盖）……输出每个流最多 2000 行，更长会存到临时文件"，让模型对行为有正确预期；以及用 `schemars` 从 Rust 结构体 `ShellParams` 自动生成的 JSON Schema 参数定义。模型按 schema 填参数（比如 `{"command": "ls -la"}`），goose 用 `serde_json::from_value` 反序列化成强类型结构体再执行（`developer/mod.rs:92-99` 的 `parse_args`）——参数不合法时不会崩溃，而是把解析错误作为工具结果喂回给模型，让它自己纠正（`developer/mod.rs:199-205`）。这种"错误也是对话内容"的设计贯穿所有工具。

### 权限：四种模式 + 五级裁决

goose 的"油门与刹车"是 `GooseMode`（`crates/goose-provider-types/src/goose_mode.rs:22-31`）：

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

每个工具请求执行前都要过 `PermissionInspector`（`crates/goose/src/permission/permission_inspector.rs:144-196`），裁决顺序像机场安检：

```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]
```

几个细节值得说：

- **SmartApprove 靠两个信息源**：MCP 工具自带的 `readOnlyHint` 注解（`permission_inspector.rs:172-176`），和一个 **LLM 裁判**（`permission/permission_judge.rs`，提示词在 `prompts/permission_judge.md`）——让一个（通常更小的）模型判断"这个调用是只读的吗"。判定结果会被缓存（`cache_non_readonly_decision`，`permission_inspector.rs:22-34`）。
- **装扩展永远要问**：`manage_extensions` 工具被特殊对待（`permission_inspector.rs:177-181`），因为装扩展=引入新代码，风险最高。
- **找不到裁决依据时默认问用户**（`permission_inspector.rs:191-194`）——宁可烦人，不可放行。
- 权限之上还有**安全检查器**可以否决已批准的调用：`crates/goose/src/security/` 下有提示注入扫描（`scanner.rs`、`patterns.rs`）、外传检测（`egress_inspector.rs`）等，其结果作为"override"覆盖权限结论（`permission_inspector.rs:116-128`）。另外还有**钩子（hooks）**系统，在 `PreToolUse`、`BeforeShellExecution` 等 10 个事件点（`hooks/mod.rs:67-76`）允许外部插件拦截。

### shell 执行的安全设计

developer 扩展的 `shell` 工具有几层考虑（`developer/shell.rs`、`developer/mod.rs:127-146`）：

1. **默认不用登录 shell**：自动检测时优先 `bash`、退而 `sh`（`shell.rs:141-145`），注释解释 LLM 常写出语法花哨的命令，登录 shell 差异太大反而易错；想要自己的 shell 可用 `GOOSE_SHELL` 覆盖（`shell.rs:87-96`）。Windows 默认 `cmd`。
2. **输出限流**：stdout/stderr 分离返回，每个流最多 2000 行，超长写入临时文件（`developer/mod.rs:133-134`）——防止一条 `cat` 大文件把上下文窗口撑爆。
3. 工具注解标了非只读（`developer/mod.rs:140-146`），所以在 SmartApprove 模式下会触发审批；还有 `BeforeShellExecution` 钩子可二次拦截。

---

## 8. 上下文压缩与记忆：鹅的"金鱼脑"自救术

模型上下文窗口再大也会满。goose 有三层应对：

### 第一层：主动压缩（compaction）

`crates/goose/src/context_mgmt/mod.rs:213` 的 `check_if_compaction_needed()` 在 token 用量超过阈值（默认 `DEFAULT_COMPACTION_THRESHOLD = 0.8`，即可用窗口的 80%，`mod.rs:24`）时触发。压缩由 `compact_messages()`（`mod.rs:76`）执行，做法很"体面"：

- **不删历史**，而是把旧消息标记为"对用户可见、对模型不可见"（`mod.rs:139-149` 用 `with_agent_invisible()`）——你在界面上还能翻看完整记录，但模型只看到摘要；
- 让 LLM 生成一份摘要消息，标记为"仅模型可见"（`mod.rs:151`）；
- 追加一段"衔接台词"，告诉模型"你的上下文被压缩了，别声张，接着干活"（`mod.rs:34-47`，三种场景三种措辞：普通对话、工具循环中、用户手动 `/compact`）；
- 非手动压缩时**保留最近一条纯文本用户消息**（`mod.rs:101-132`），保证模型记得"用户最后到底要什么"。

第 6 章说过，provider 直接报 `ContextLengthExceeded` 时也会兜底压缩一次。

### 第二层：工具调用对摘要（tool pair summarization）

长会话里最占地方的往往是工具输出（比如几千行测试日志）。`maybe_summarize_tool_pairs()`（`context_mgmt/mod.rs:624`）会在后台把"工具请求+响应"成对地交给 LLM 摘要，每批 10 对（`mod.rs:26`），默认可用 `GOOSE_TOOL_PAIR_SUMMARIZATION` 关掉（`mod.rs:28-32`）。阈值用 `compute_tool_call_cutoff` 计算（`mod.rs:498`），主循环在 `agent.rs:2051-2062` 每轮启动一次（整个回合只做一次，见 `tool_pair_summarization_done` 标志）。

**举个对比例子感受这两层的分工**：agent 连跑了 30 次 shell，每次输出 500 行测试日志——这时整体 token 还没到 80% 阈值，第一层不触发，但第二层会发现"工具调用对"太多，把早期的 `shell` 输出摘要成"第 1-3 次测试：均因缺少 mock 失败"，瞬间省下上万 token；而当整个会话（包括摘要后的内容）继续膨胀到阈值时，第一层才出手，把整段历史折叠成一份摘要。前者是"随手整理桌面"，后者是"打包搬家"。

### 第三层：记忆（memory）

压缩是"短期记忆缩水"，真正的**长期记忆**由两个扩展提供：

- **memory 扩展**：`remember_memory` 把事实按"分类 + 标签"存到本地（`crates/goose-mcp/src/memory/mod.rs:335-452`），`retrieve_memories` 按分类取回。比如让 goose 记住"部署服务器在 10.0.0.8"，下个会话也能查。
- **chatrecall 扩展**：搜索历史会话、加载旧会话摘要，相当于"翻以前的聊天记录"。

会话本身存在本地 **SQLite** 数据库 `sessions.db`（`crates/goose/src/session/session_manager.rs:28`），老版本的 `.jsonl` 会话文件有迁移逻辑（`session/legacy.rs`）。会话名还是 LLM 自动起的（`session/session_naming.rs`，提示词 `prompts/session_name.md`，主循环 `agent.rs:1892-1907` 异步生成）。

```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. 子 agent 与多 agent：鹅群协作

goose 的多 agent 能力主要藏在两个平台扩展里：

### summon 扩展：`delegate` 派生子代理

`summon`（"召唤"）提供两个工具（`platform_extensions/summon.rs:576, 649`）：

- `load`：把"知识源"加载进当前上下文——可以是文件、URL，也可以是 **sub-recipe（子配方）**；
- `delegate`：把任务**委派给一个独立子代理**执行。子代理有自己的会话、自己的上下文窗口，跑完只把结论文本带回来（`subagent_handler.rs:48-63` 的 `run_subagent_task`，最后 `extract_response_text` 提取文本）。

子代理的可用来源包括"当前 recipe 的 sub_recipes"（`summon.rs:1148`）。子代理有自己的系统提示词模板（`prompts/subagent_system.md`），也可以有独立模型配置（`subagent_task_config.rs:15`）。

### sub-recipe：配方里的"子任务卡"

Recipe（配方，下一章详述）可以声明 `sub_recipes`（`crates/goose/src/recipe/mod.rs:82`），每个子配方有 `name`、`path`、`values`（传参）、`sequential_when_repeated`（重复调用时是否排队串行，`mod.rs:119-128`）。主 agent 通过 summon 的 `delegate` 调用它们，实现"主厨指挥帮厨"。

### orchestrator 扩展：多会话总控

`orchestrator`（默认关闭、对 UI 隐藏，`platform_extensions/mod.rs:164-176`）是给"agent 管理 agent"准备的：`list_sessions` 看有哪些 agent 会话、`start_agent` 在指定目录起新会话、`send_message` 发消息、`interrupt_agent` 打断（`orchestrator.rs:593-619`）。CLI 的 `goose review`（代码评审）就用这类编排并行检查多个维度。

```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/>没有子代理的中间过程
```

这种设计的好处是**上下文隔离**：子代理翻几十个文件产生的噪音不会污染主会话，主 agent 只看到"报告"。代价是信息有损——子代理踩过的坑主 agent 不知道。

**再对比一种用法体会两条路线的差别**：如果你只是想让 agent "先了解一下这个目录再决定怎么做"，用 `summon::load` 把知识（文件内容、URL 正文、子配方说明）拉进**当前**上下文即可，主 agent 亲自消化；如果任务边界清晰、过程注定冗长（"把这 50 个文件逐个加上 license 头"），用 `delegate` 派给子代理更合适——主会话只收一句"全部完成，3 个文件有冲突需人工处理"。前者像"把资料递到同事桌上"，后者像"把整块工作外包出去"。

---

## 10. 生态：MCP、配方与命令清单

### MCP 是 goose 的"USB 接口"

goose 几乎所有外部能力都通过 **Model Context Protocol** 接入（Rust 端用 `rmcp` 1.4，`Cargo.toml:23`）。对用户来说，"装扩展"就是登记一个 MCP 服务器：一条命令（stdio）、一个 URL（streamable HTTP）、或一个内置名字。对模型来说，还有一个元能力——`search_available_extensions` 和 `manage_extensions` 工具让它**自己发现、自己装扩展**（`ext_manager.rs:269-344`），装完工具列表立即刷新（主循环 `agent.rs:2338-2343` 里 `tools_updated` 触发重建提示词）。当然，如第 7 章所说，装扩展强制需要用户批准，而且新扩展会经过恶意检查（`agents/extension_malware_check.rs`）。

### Recipe（配方）：可以分享的"工作流剧本"

Recipe 是 goose 最有辨识度的概念之一：一个 **YAML 或 JSON 文件**（`recipe/mod.rs:25`），描述一个完整任务怎么跑。结构（`recipe/mod.rs:41-86`）：

| 字段 | 作用 |
|---|---|
| `title` / `description` | 名字与说明（必填） |
| `instructions` | 给模型的系统级指令（与 `prompt` 至少填一个） |
| `prompt` | 会话开场白 |
| `extensions` | 这个配方需要哪些扩展 |
| `settings` | 指定 provider/模型/温度/最大轮次（`mod.rs:98-110`） |
| `parameters` | 让用户填的变量（模板渲染用） |
| `activities` | 桌面端显示的"建议动作"小药丸 |
| `response` | **JSON Schema**：要求最终输出符合的结构（配合 final_output 工具强制校验，`final_output_tool.rs`） |
| `sub_recipes` | 子配方（第 9 章） |
| `retry` | 失败自动重试策略（主循环 `agent.rs:2765` 的 `handle_retry_logic`） |

配方支持模板变量（`template_recipe.rs`）、深链接分享（`goose recipe deeplink`，`recipe_deeplink.rs`）、安全扫描（仓库根的 `recipe-scanner/`），还有官方示范（`workflow_recipes/`、`examples/`、`CONTRIBUTING_RECIPES.md`）。`goose run --recipe xxx.yaml` 即可无人值守执行——`goose-self-test.yaml` 就是项目自己的冒烟测试（`AGENTS.md:71`）。

**一个想象中的最小配方**，用来体会各字段如何拼合：

```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
```

把这个文件发给同事，他 `goose run --recipe standup.yaml` 就能复现一模一样的流程——这正是 recipe 想解决的痛点：**agent 的"玩法"第一次可以像脚本一样被版本管理、评审和分享**。

### CLI 命令清单（`cli.rs` 的 `Command` 枚举）

`configure`（初始配置）、`session`（会话：list/remove/export/diagnostics…）、`run`（执行指令文件/recipe，可无人值守）、`recipe`（validate/deeplink/list）、`schedule`（定时任务 add/list/remove/run-now）、`mcp`（单独跑某个捆绑 MCP 服务器）、`acp`（stdio ACP 服务器）、`serve`（HTTP/WebSocket ACP 服务器）、`skills`、`plugin`（从 git 安装插件）、`project`（项目目录管理）、`term`（终端集成）、`tui`（实验终端 UI）、`local-models`（本地模型管理）、`review`（代码评审）、`gateway`（远程网关）、`update`、`doctor`（自检）、`info`、`completion`（shell 补全）、`validate-extensions`。

### 自定义扩展与技能

除了写 MCP 服务器，还有几条轻量路径：**Frontend 工具**（Desktop 端用 TypeScript 给模型提供能力）、**InlinePython 扩展**、**plugin**（git 仓库形式的扩展包，`commands/plugin.rs`）、以及 **skills 技能**——在 `~/.agents/skills` 或项目 `.agents/skills` 放 `SKILL.md`（`crates/goose/src/skills/mod.rs:38-45`），模型用 `load_skill` 工具按需加载详细指令，避免一次性把所有知识塞进系统提示词。

```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. 功能特性：模型、认证与特色功能

### 提供商（Provider）阵容

这是 goose 最"豪华"的部分。注册表在 `crates/goose/src/providers/init.rs:62-150`，分三类：

1. **手写 provider**：Anthropic、OpenAI、Google、Ollama、OpenRouter、Azure OpenAI、AWS Bedrock、GCP Vertex、Databricks（两代）、GitHub Copilot、HuggingFace、LiteLLM、NanoGPT、SageMaker TGI、Snowflake、Tetrate、xAI（API key 与 OAuth 两种）、Avian；
2. **"订阅复用"型**：`ClaudeCodeProvider`（复用 Claude 订阅）、`ChatGptCodexProvider` / `CodexProvider`（复用 ChatGPT/Codex 订阅）、`GeminiCliProvider`（复用 Gemini CLI 登录）、`KimiCodeProvider`、`CursorAgentProvider`——这些通过 CLI 或 OAuth 蹭你已有的账号，不用单独买 API；
3. **ACP provider**：Amp、Claude、Codex、Copilot、Pi（`init.rs:62-93` 的 `*_acp` 系列），即把别的 agent 当模型后端使；
4. **声明式 provider**：37 个 JSON 定义（`crates/goose-providers/src/declarative/definitions/`）——DeepSeek、Groq、Mistral、Moonshot（月之暗面）、智谱（zhipu）、阿里（alibaba）、Cerebras、Fireworks、LM Studio、Together 等 OpenAI 兼容端点，加新提供商只要加一个 JSON 文件。

**本地模型**也支持：Ollama 走 HTTP API；`goose-local-inference` crate 内置 llama.cpp（`llama-cpp-2`，`Cargo.toml:104`）和 candle 运行时，配合 `goose local-models`（别名 `goose lm`）命令从 HuggingFace 搜索、下载 GGUF/MLX 模型（`cli.rs:1201` 起）。认证方式覆盖 API key、OAuth 设备码流程（`providers/oauth_device_flow.rs`）、系统钥匙串存储（`keyring` crate）等。

### 模型选择与 lead/worker

模型通过 `/model` 斜杠命令、`GOOSE_PROVIDER`/`GOOSE_MODEL` 配置、或 recipe 的 `settings` 指定。**需要诚实说明**：在当前 commit 的代码中，我**未找到**早期 goose 版本曾有过的 lead/worker 双模型模式（`GOOSE_LEAD_MODEL` 相关代码已不存在）——相近能力如今通过子代理独立模型配置（`subagent_task_config.rs`）和 recipe 设置体现。

### 特色功能

- **定时任务（schedules）**：`crates/goose/src/scheduler.rs` 基于 `tokio-cron-scheduler` 实现 cron 调度，`goose schedule add "0 9 * * *" --recipe daily.yaml` 就能让 goose 每天 9 点自动跑某个配方；`schedule sessions` 还能查某个定时任务产生过的会话。
- **会话管理**：SQLite 存储、LLM 自动命名、`session export` 导出、resume/fork（恢复/分叉）、`diagnostics` 诊断打包。
- **计划模式（plan mode）**：`/plan` 进入、`/endplan` 退出；CLI 会先让模型出计划并分类"是计划还是澄清问题"（`session/mod.rs:236-270`）。
- **goal / grind**：`/goal` 设一个目标，agent 每次想收工都会被"隐形催促"直到目标达成（`agent.rs:2720-2755`）；`grind` 是更强硬的版本——"没做完就继续磨"。
- **Apps**：模型可以生成沙盒 HTML/JS 小应用，在独立窗口运行（`apps.rs`）——goose 版"Artifacts"。
- **钩子与遥测**：10 个生命周期钩子事件供插件介入；PostHog 遥测为显式 opt-in，OpenTelemetry 可导出 traces/metrics。

**把几个特色功能串成一个真实场景**：周一早上 8:55，调度器按 cron 触发"整理上周 issue"配方，goose 无头跑完并把结果写进一个新会话；你 9 点打开桌面端，在会话列表里看到它（名字是 LLM 自动起的"上周 issue 汇总"），接着用 `/goal` 设定"今天把优先级最高的 bug 修掉"，agent 开始干活——每次它以为可以收工时，goal 催促都会把它拉回来继续；中途你觉得某个改动危险，敲 `/mode smart_approve` 收紧油门，之后的写文件操作都会先弹窗问你。这一整套流程没有离开过 goose，这就是"调度 + 会话 + 目标 + 权限"组合拳想覆盖的日常。

```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. 总结：设计哲学与取舍

逛完整个园子，可以提炼 goose 的几个"独特想法"：

1. **一切能力都是扩展，包括内核能力**。读写文件、待办清单、代码分析这些"标配"在 goose 里也是扩展，和第三方 MCP 服务器走同一条流水线（权限、审批、工具命名、提示词注入）。但需注意：`Builtin`（进程内管道）和 `Platform`（纯 Rust 直接写在 agent 进程里）两种扩展形态**不走 MCP 协议**——它们是 goose 为挽回"每个 MCP 扩展一个进程"的性能开销而发明的"作弊"形态。
2. **引擎即服务器，GUI 只是客户端**。`goose serve` 把 agent 变成本地 ACP 服务，桌面端、Zed 编辑器、任何第三方都能接入。这把"agent"从"一个终端程序"提升为"一种本机基础设施"，是鹅最有远见的赌注。
3. **为提示词缓存而工程化**。小时级时间戳、稳定的工具排序、静态系统提示 + 动态 MOIM 便签分离——处处可见对 prompt cache 命中的执念，因为缓存命中就是真金白银的延迟和费用。
4. **Recipe 作为可分享的"工作流单元"**。把"指令 + 扩展 + 参数 + 输出 schema + 重试"打包成一个 YAML，配合深链接和定时调度，goose 试图让 agent 工作流像 npm 包一样流通。
5. **权限用"模式"而不是"规则表"做主线**。Auto/Approve/SmartApprove/Chat 四档简单好懂，再辅以只读注解、LLM 裁判、安全扫描器、钩子兜底。取舍很明确：普通用户不会被复杂的规则语法吓退，进阶需求交给扩展和钩子。

**主要取舍（trade-offs）**：Rust 带来单文件部署和性能，但异步 Rust 的学习曲线筛掉了一部分潜在贡献者；MCP 进程模型换来生态兼容，换来的是扩展启动延迟和故障面；SmartApprove 的 LLM 裁判聪明但引入了"用模型判断模型"的信任问题；扩展动态启停很灵活，但 `MAX_EXTENSIONS = 5` / `MAX_TOOLS = 50` 的建议上限（`prompt_manager.rs:19-20`）也承认了一个现实——工具太多，再强的模型也会挑花眼。

如果用一句话总结这只鹅：**它把自己设计成"模型中立、协议开放、可以被任何东西骑"的本地 agent 引擎**——这和 Claude Code"围绕单一模型把终端体验打磨到极致"的路线，恰好构成开源 agent 世界的两种代表性答案。

```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
```

---

*（全文完。所有源码引用基于 commit d17d65f；如需核对，可在仓库目录执行 `git show <commit>:<文件路径>` 查看对应内容。）*
