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.rsfiles (Cargo workspace, 12 crates) + TypeScript 228.ts/ 361.tsx(Electron desktop etc.), workspace version1.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 inGOVERNANCE.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, seeAGENTS.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.”
flowchart LR
U[用户] -->|打字| CLI["goose CLI
(Rust)"]
U -->|点击| GUI["Goose Desktop
(Electron)"]
U -->|HTTP/WS| API["goose serve
(ACP Server)"]
CLI --> CORE["crates/goose
核心库 (Agent)"]
GUI --> API
API --> CORE
CORE -->|调用| LLM["15+ 模型提供商
Anthropic/OpenAI/Ollama..."]
CORE -->|MCP 协议| EXT["70+ 扩展
文件/浏览器/数据库..."]
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
gooselibrary directly and callsAgent::reply()in-process. - Desktop does not link the Rust library; it spawns a
goose servechild process—an ACP-over-HTTP/WebSocket server (crates/goose-cli/src/cli.rs:844, "Start ACP server over HTTP and WebSocket"). Electron main processui/desktop/src/gooseServe.ts:320starts 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.
flowchart TD
subgraph 外壳层
A["goose-cli
(crates/goose-cli)"]
B["Goose Desktop
(ui/desktop, Electron)"]
C["第三方程序
(Zed 等编辑器)"]
end
subgraph 协议层
D["直接函数调用"]
E["ACP over WebSocket/HTTP
goose serve"]
end
subgraph 核心层 crates/goose
F["Agent 主循环
agents/agent.rs"]
G["ExtensionManager
扩展管理"]
H["PromptManager
提示词组装"]
I["权限/安全/压缩"]
end
subgraph 外部世界
J["Provider API
(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”:
// 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:
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):
sequenceDiagram
participant E as Electron 主进程 (main.ts)
participant S as goose serve 子进程
participant R as React 渲染进程
E->>S: spawn "goose serve --port N"
(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):
- If it does not start with
/→ wrap asInputResult::Message, send to the agent; - If it starts with
/→ hand tohandle_slash_command()(input.rs:221) to match built-in commands.
All possible results are defined in the InputResult enum at input.rs:15-36:
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.
flowchart TD
I[用户输入一行文字] --> Q{以 / 开头?}
Q -->|否| M["InputResult::Message
→ 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["当作普通消息发给模型
(让模型自己理解)"]
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:
.goosehintsandAGENTS.md(load_hints.rs:10-11); the config itemCONTEXT_FILE_NAMEScan change them to any filename (load_hints.rs:13-24)—for example make it readCLAUDE.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 (testload_hints.rs:505-535verifies this). - Supports
@fileimport: writing@docs/api.mdin a hint file inlines that file’s content, but the import boundary is the git root—an out-of-bound reference like@../../../forbidden.mdis refused (testload_hints.rs:635-688), and files ignored by.gitignore(e.g.secret.env) are not read even if@’d (testload_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 agentwritesnested/foo.rsor shellscat nested/doc.md, it recordsnested/as a “directory to check”; next round if a.goosehintsis found there it is auto-appended to the system prompt (the agent loop atagent.rs:2666-2676rebuilds 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.
flowchart TD
subgraph 系统提示词 system prompt
A["system.md 模板
(身份+扩展说明)"]
B[".goosehints / AGENTS.md
全局+项目层, 支持 @导入"]
C["frontend 指令
(桌面端注入)"]
D["模式提示
chat 模式禁用工具"]
end
subgraph 每轮消息 messages
E["历史对话
(压缩后)"]
F["MOIM turn_context 便签
时间/目录/预算/token 用量"]
G["用户最新消息"]
end
A & B & C & D --> H["provider.stream(
system_prompt, messages, tools
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:
flowchart TD
START["用户消息到达 reply()
agent.rs:1549"] --> PRE["prepare_reply_context
修会话+拼提示词+取工具
agent.rs:734"]
PRE --> LOOP{"主循环
agent.rs:1948"}
LOOP -->|已取消?| OUT1[CancellationToken → 退出]
LOOP --> STEER["排出插队消息 drain_pending_steers
agent.rs:1953"]
STEER --> TURN["turns_taken += 1
超过 max_turns(默认1000) 就收尾
agent.rs:2018-2024"]
TURN --> MOIM["注入 MOIM 动态上下文
agent.rs:2026"]
MOIM --> REQ["stream_response_from_provider
请求模型(流式)
reply_parts.rs:287"]
REQ --> PARSE{"流式解析响应
agent.rs:2079"}
PARSE -->|纯文本,无工具| TXT["追加 assistant 消息
no_tools_called=true"]
PARSE -->|有工具调用| CAT["categorize_tools 分类
agent.rs:2124"]
CAT --> PERM["权限检查器逐个裁决
agent.rs:2210-2232"]
PERM --> EXEC["执行工具(并发)
前端工具/已批准/待审批
agent.rs:2244-2336"]
EXEC --> APPEND["请求+结果成对追加进会话
agent.rs:2458-2524"]
APPEND --> LOOP
TXT --> CHECK{"终止条件?
agent.rs:2697+"}
CHECK -->|final_output 已提交| DONE[结束循环]
CHECK -->|设定了 goal/grind| NUDGE["塞一条隐形催促消息
agent.rs:2720-2755"] --> LOOP
CHECK -->|空响应| RETRY["最多重试 3 次
agent.rs:2778"] --> LOOP
CHECK -->|否则| DONE
PARSE -->|ContextLengthExceeded| COMPACT["compact_messages 压缩
agent.rs:2562"] --> LOOP
Beat by beat:
- Cancel check (
agent.rs:1949): at the start of each round look atCancellationToken—this is the brake when the user presses ESC/Ctrl+C. During tool execution it is likewise listened to insidetokio::select!(agent.rs:2276-2281). - 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 aUserPromptSubmithook. - Turn counting:
turns_takencap defaults toDEFAULT_MAX_TURNS = 1000(agent.rs:69), also configurable withGOOSE_MAX_TURNS(agent.rs:1931-1935). Over the cap, outputMAX_TURNS_MESSAGEand wrap up. - 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 callprovider.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. - Stream parse:
while let Some(next) = stream.next().await(agent.rs:2079) reads chunk by chunk. Pure text/thinking content isyield AgentEvent::Messagepushed to the UI; tool calls go throughcategorize_tools(agent.rs:2124) into two classes: frontend tools (executed by the frontend/Desktop, e.g. “show a chart on screen”) and ordinary tools. - 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. - Append and request again: tool-request messages and response messages are appended in pairs (
agent.rs:2458-2524), thenloopreturns to the top and requests the model again—the classic “request → tool → append → request again” agent loop. Note the large stretch atagent.rs:2346-2456handling 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. - 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 mostMAX_EMPTY_TURN_RETRIES = 3times,agent.rs:73, 2778-2785). If none of those, the loop truly ends. - Context-overflow self-heal: if the provider reports
ContextLengthExceeded(agent.rs:2532), first tell the user “compacting,” callcompact_messagesto 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 balanceCreditsExhausted, refusalRefusal, 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):
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:
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 且工具带
只读注解 readOnlyHint?"}
RO -->|是| A6[放行]
RO -->|否| EX{"是 manage_extensions
(装扩展)?"}
EX -->|是| A7[强制问用户
"装扩展涉及安全"]
EX -->|否| LLM{"SmartApprove?"}
LLM -->|是| J[LLM 裁判判断只读/写
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
readOnlyHintannotation (permission_inspector.rs:172-176), and an LLM judge (permission/permission_judge.rs, prompt inprompts/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_extensionstool 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 includingPreToolUse,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):
- Default is not a login shell: auto-detect prefers
bash, falls back tosh(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 withGOOSE_SHELL(shell.rs:87-96). Windows defaults tocmd. - 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 onecatof a large file from blowing the context window. - The tool annotation marks it non-read-only (
developer/mod.rs:140-146), so in SmartApprove mode it triggers approval; there is also aBeforeShellExecutionhook 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-149useswith_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_memorystores facts by “category + tags” locally (crates/goose-mcp/src/memory/mod.rs:335-452);retrieve_memoriesretrieves 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).
flowchart LR
subgraph 写入路径
A[每轮对话] --> B["token 用量 ≥ 80%?"]
B -->|是| C["compact_messages
旧消息→隐形, LLM 摘要→可见"]
A --> D["工具输出太多?"]
D -->|是| E["tool pair 摘要
每批 10 对"]
end
subgraph 长期记忆
F["memory 扩展
分类+标签"]
G["chatrecall 扩展
搜索旧会话"]
H["sessions.db
(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_taskatsubagent_handler.rs:48-63, finallyextract_response_textextracts 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.
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
(subagent_handler.rs:48)
Note over A: 独立上下文/独立工具执行
跑自己的 reply 主循环
A-->>S: 最终文本结论
S-->>M: 作为工具结果返回
Note over M: 主 Agent 上下文里只有结论
没有子代理的中间过程
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).
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.
flowchart TD
subgraph 扩展来源
A["MCP 服务器
(stdio/HTTP)"]
B["捆绑扩展
goose-mcp"]
C["平台扩展
Rust 进程内"]
D["Frontend/InlinePython"]
E["Skills 技能文件
SKILL.md"]
F["Recipes 配方
YAML/JSON"]
end
A & B & C & D --> EM["ExtensionManager
统一孵化/管理"]
EM --> T["工具列表
ext__tool 命名"]
E --> T2["load_skill 按需加载"]
F --> R["会话配置:
指令+扩展+参数+重试"]
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:
- 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;
- “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; - ACP providers: Amp, Claude, Codex, Copilot, Pi (the
*_acpseries atinit.rs:62-93), i.e. use another agent as the model backend; - 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.rsimplements cron scheduling ontokio-cron-scheduler;goose schedule add "0 9 * * *" --recipe daily.yamllets goose auto-run a recipe at 9 every day;schedule sessionscan also look up sessions a scheduled task has produced. - Session management: SQLite storage, LLM auto-naming,
session export, resume/fork,diagnosticsdiagnostic packing. - Plan mode:
/planenter,/endplanexit; 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:
/goalsets a goal; every time the agent wants to clock out it is “invisibly nudged” until the goal is met (agent.rs:2720-2755);grindis 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.
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:
- 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) andPlatform(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.” - The engine is the server; the GUI is only a client.
goose serveturns 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. - 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.
- 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.
- 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.
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.)