M01 · SOURCE-GROUNDED TUTORIAL
Goose从源码学会它怎么工作 MCP 原生、扩展友好、单循环清楚;安全边界更依赖工具检查而非 OS 沙箱。 我们不把 README 当结论,而是沿主循环、工具、上下文、权限、扩展、协作和状态一路读到实现。
Rust · MCP-first 通用 Agent Apache-2.0 021b0db8dbee 18 个结论 · 43 处引用
这门课怎么读
先建立直觉,再沿一条任务链钻进代码 参考教程的做法不是把 API 名称罗列出来,而是从一个小白能理解的问题开始,先解释“为什么需要这个机制”,再用概念对比、执行链路和固定提交的源码回答“它究竟怎么做”。本页把 Goose 的 18 个源码结论重新编排成十节课;每个结论都保留证据等级、文件路径、行号和可点击源码。
你会得到 一张可复述的架构地图 一次完整任务的链路追踪 能迁移到自研 Harness 的设计判断
你不会得到 把 README 功能当成已验证事实 把 prompt 约束说成 OS 沙箱 把一次双模型调用夸成多 Agent 平台
M00 · MAP
先看全景:这个 Agent 的控制面在哪里 下面的图不是产品宣传图,而是把固定提交里最关键的入口、循环、模型、工具、安全、状态和协作节点放在一张地图上。
核心机制 单一流式循环;完整 tool call 后并发;防失控上限
M00.5 · TRACE
跟踪一个任务:从输入到交付 把下面九步当成你读源码时的“地图坐标”。每到一个节点,都能回到后面的章节查具体实现。
01 提交目标与会话配置 任务进入
↓
02 装配 system / path hints / skills 把结果交给下一层
↓
03 流式请求 把结果交给下一层
↓
04 返回文本与完整 tool calls 把结果交给下一层
↓
05 危险优先检查权限 把结果交给下一层
↓
06 并发执行同轮工具 把结果交给下一层
↓
07 持久化消息、成本与 trace 把结果交给下一层
↓
08 需要时结构化压缩 把结果交给下一层
↓
09 回传最终答复 交付/续跑
读图提醒 箭头只表示控制面之间的关系,不代表每个实现都同步、串行或拥有 OS 隔离;真正的边界要以对应章节的源码摘录和 caveat 为准。
M01 · ORIENTATION
先把 Agent 看成一台会交付的机器 如果只看 README,你知道它能做什么;钻进源码后,我们要知道它为什么能做、什么时候会停、失败后谁负责收拾。
先用一个生活比喻 把 Agent 想成一间带传送带的工作室:入口收任务,主循环决定下一步,模型负责提出动作,工具负责动手,状态账本负责让下一班人接着干。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
读源码时先问 本课的判断方式 这一层有没有独立证据? 没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。 谁拥有最终控制权? 区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念 固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。
小练习 1 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M02 · LOOP
主循环:模型为什么会继续动 一次模型调用为什么会变成十几步?循环靠什么继续,靠什么停止?
先用一个生活比喻 像一个会看回执的快递员:模型先写行动单,工具返回回执,主循环把回执放回桌面,再让模型决定下一张行动单。
这套实现先回答了什么? 它不是“模型回答一次就结束”,而是模型说一步、系统做一步、把结果再交回模型,直到满足结束条件。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 单一流式 Agent 循环驱动推理、工具和持久化 crates/goose/src/agents/agent.rs:1930它不是“模型回答一次就结束”,而是模型说一步、系统做一步、把结果再交回模型,直到满足结束条件。 结束条件有防失控上限 crates/goose/src/agents/agent.rs:67即使模型什么都不返回,或插件一直说“还不能停”,Goose 也不会永远卡住。
01 L1 · fact · goose-loop-001
单一流式 Agent 循环驱动推理、工具和持久化
先看源码事实 reply_internal 在一个有界 loop 中调用 provider stream,消费增量消息,执行工具,把结果加入会话,再决定继续、重试、压缩或结束。
翻译成白话 它不是“模型回答一次就结束”,而是模型说一步、系统做一步、把结果再交回模型,直到满足结束条件。
为什么这对自研重要 Harness 的真正核心是状态机而不是提示词;重试、转向、停止钩子和上下文恢复都进入同一控制环。
边界与风险 默认最大 turn 是 1000,不代表每次任务都会接近该值;会话配置和环境参数可覆盖。
固定提交源码摘录 复制
1930 let inner = Box::pin(async_stream::try_stream! {
1931 let mut turns_taken = 0u32;
1932 let max_turns = session_config.max_turns.unwrap_or_else(|| {
1933 Config::global()
1934 .get_param::<u32>("GOOSE_MAX_TURNS")
1935 .unwrap_or(DEFAULT_MAX_TURNS)
1936 });
1937 let mut compaction_attempts = 0;
1938 let mut empty_turn_retries = 0u32;
1939 let mut retrying_after_empty_turn = false;
1940 let mut last_assistant_text = String::new();
1941 let mut goal_check_pending = false;
1942 let mut tool_pair_summarization_done = false;
1943 let mut stop_hook_handled_for_exit = false;
1944 let mut retrying_after_stop_hook_denial = false;
1945 let mut consecutive_stop_hook_blocks = 0u32;
1946 let stop_hook_block_cap = self.stop_hook_block_cap();
1947 let mut can_drain_pending_steers = false;
1948
1949 loop {
1950 if is_token_cancelled(&cancel_token) {
1951 break;
1952 }
1953
1954 if can_drain_pending_steers {
… 78 lines omitted; exact range 1930–2043 …
2033 ).await;
2034
2035 let mut stream = Self::stream_response_from_provider(
2036 self.provider().await?,
2037 model_config.clone(),
2038 &session_config.id,
2039 &system_prompt,
2040 conversation_with_moim.messages(),
2041 &tools,
2042 &toolshim_tools,
2043 ).await?;
为什么相信这条结论?查看 2 处证据
02 L1 · fact · goose-loop-002
结束条件有防失控上限
先看源码事实 空模型响应最多重试 3 次;阻塞停止的插件 hook 连续超过可配置上限后会被强制覆盖,避免无限循环。
翻译成白话 即使模型什么都不返回,或插件一直说“还不能停”,Goose 也不会永远卡住。
为什么这对自研重要 所有“让 Agent 继续”的机制都需要配额和最终逃生门。
固定提交源码摘录 复制
67 use tracing::{debug, error, info, instrument, warn};
68
69 const DEFAULT_MAX_TURNS: u32 = 1000;
70 const DEFAULT_STOP_HOOK_BLOCK_CAP: u32 = 8;
71 const COMPACTION_PROGRESS_TEXT: &str = "goose is compacting the conversation...";
72 const MAX_TURNS_MESSAGE: &str = "I've reached the maximum number of actions I can do without user input. Would you like me to continue?";
73 const MAX_EMPTY_TURN_RETRIES: u32 = 3;
74 const EMPTY_TURN_MESSAGE: &str =
75 "The model returned an empty response. Please resend your message to continue.";
76 const DEFAULT_FRONTEND_INSTRUCTIONS: &str = "The following tools are provided directly by the frontend and will be executed by the frontend when called.";
77
78 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
79 enum ToolCategory {
为什么相信这条结论?查看 3 处证据
小练习 2 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M03 · MODEL
模型调用:流式输出如何变成可执行步骤 模型输出的文字、思考、工具调用和错误,经过哪些转换才进入 Agent 状态?
先用一个生活比喻 模型像电话另一端的同事:你听到的不是一整段录音,而是一串实时片段;Harness 要边听边拼装,还要能在电话断线时留下可恢复的记录。
这套实现先回答了什么? 不同模型厂商先被翻译成同一种“消息水管”。普通文字可以一个词一个词流出,但工具参数不能半截就执行。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 Provider 以流式协议统一,工具调用必须完整再上送 crates/goose-provider-types/src/base.rs:281不同模型厂商先被翻译成同一种“消息水管”。普通文字可以一个词一个词流出,但工具参数不能半截就执行。
03 L2 · fact · goose-provider-001
Provider 以流式协议统一,工具调用必须完整再上送
先看源码事实 Provider trait 只要求实现 stream(model, system, messages, tools);MessageStream 可以增量返回文本,但工具调用以完整对象返回。
翻译成白话 不同模型厂商先被翻译成同一种“消息水管”。普通文字可以一个词一个词流出,但工具参数不能半截就执行。
为什么这对自研重要 Provider 适配层承担格式差异,Agent 主循环不需要为每家 API 复制控制逻辑。
固定提交源码摘录 复制
281 /// A message stream yields partial text content but complete tool calls, all within the Message object
282 /// So a message with text will contain potentially just a word of a longer response, but tool calls
283 /// messages will only be yielded once concatenated.
284 pub type MessageStream = Pin<
285 Box<dyn Stream<Item = Result<(Option<Message>, Option<ProviderUsage>), ProviderError>> + Send>,
286 >;
为什么相信这条结论?查看 2 处证据
小练习 3 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M05 · CONTEXT
上下文:有限窗口怎样装下长任务 当对话、工具输出、计划和记忆越来越多,系统怎样决定留下什么、折叠什么、放到哪里?
先用一个生活比喻 上下文不是聊天记录,而是一张会整理的工作台:常用零件放桌面,旧材料装进档案盒,必要时只留下索引卡。
这套实现先回答了什么? 快塞满模型记忆时,它不会粗暴删掉全部历史,而是把旧进展整理成一张交接单,再把用户最新要求放回去。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 80% 阈值触发结构化压缩 crates/goose/src/context_mgmt/mod.rs:26快塞满模型记忆时,它不会粗暴删掉全部历史,而是把旧进展整理成一张交接单,再把用户最新要求放回去。 上下文超限采用渐进式工具结果剥离和一次恢复重试 crates/goose/src/context_mgmt/mod.rs:319先保留尽可能多的工具证据,实在放不下才逐级清掉旧工具输出;不能靠无限压缩掩盖超限。
05 L1 · fact · goose-context-001
80% 阈值触发结构化压缩
先看源码事实 当 Agent 自管上下文且估算 token/上下文上限大于阈值(默认 0.8)时触发压缩;压缩保留最新用户文本,并插入结构化摘要与继续指令。
翻译成白话 快塞满模型记忆时,它不会粗暴删掉全部历史,而是把旧进展整理成一张交接单,再把用户最新要求放回去。
为什么这对自研重要 压缩的目标不是“越短越好”,而是保留任务状态、文件改动、错误、待办和下一步。
固定提交源码摘录 复制
26 pub const DEFAULT_COMPACTION_THRESHOLD: f64 = 0.8;
27
28 const TOOLCALL_SUMMARIZATION_BATCH_SIZE: usize = 10;
29
30 fn tool_pair_summarization_enabled() -> bool {
31 Config::global()
32 .get_param::<bool>("GOOSE_TOOL_PAIR_SUMMARIZATION")
33 .unwrap_or(true)
34 }
35
36 const CONVERSATION_CONTINUATION_TEXT: &str =
37 "Your context was compacted. The previous message contains a summary of the conversation so far.
38 Do not mention that you read a summary or that conversation summarization occurred.
39 Just continue the conversation naturally based on the summarized context.";
40
41 const TOOL_LOOP_CONTINUATION_TEXT: &str =
42 "Your context was compacted. The previous message contains a summary of the conversation so far.
43 Do not mention that you read a summary or that conversation summarization occurred.
44 Continue calling tools as necessary to complete the task.";
45
46 const MANUAL_COMPACT_CONTINUATION_TEXT: &str =
47 "Your context was compacted at the user's request. The previous message contains a summary of the conversation so far.
48 Do not mention that you read a summary or that conversation summarization occurred.
49 Just continue the conversation naturally based on the summarized context.";
为什么相信这条结论?查看 3 处证据
06 L1 · fact · goose-context-002
上下文超限采用渐进式工具结果剥离和一次恢复重试
先看源码事实 压缩会依次尝试移除 0%、10%、20%、50%、100% 的中间工具响应;Provider 明确报上下文超限时执行恢复压缩,第二次仍超限则终止。
翻译成白话 先保留尽可能多的工具证据,实在放不下才逐级清掉旧工具输出;不能靠无限压缩掩盖超限。
为什么这对自研重要 Harness 应区分账单 token、保留 token 和可牺牲的工具噪声,并给恢复动作设次数上限。
固定提交源码摘录 复制
319 async fn do_compact(
320 provider: &dyn Provider,
321 model_config: &ModelConfig,
322 session_id: &str,
323 messages: &[Message],
324 ) -> Result<(Message, ProviderUsage), anyhow::Error> {
325 let agent_visible_messages =
326 Conversation::new_unvalidated(messages.iter().cloned()).agent_visible_messages();
327
328 // Try progressively removing more tool response messages from the middle to reduce context length
329 let removal_percentages = [0, 10, 20, 50, 100];
330
331 for (attempt, &remove_percent) in removal_percentages.iter().enumerate() {
332 let filtered_messages = filter_tool_responses(&agent_visible_messages, remove_percent);
333
334 let messages_text = filtered_messages
335 .iter()
336 .map(|&msg| format_message_for_compacting(msg))
337 .collect::<Vec<_>>()
338 .join("\n");
339
340 let context = SummarizeContext {
341 messages: messages_text,
342 };
343
… 44 lines omitted; exact range 319–398 …
388 }
389 }
390 return Err(e.into());
391 }
392 }
393 }
394
395 Err(anyhow::anyhow!(
396 "Unexpected: exhausted all attempts without returning"
397 ))
398 }
为什么相信这条结论?查看 2 处证据
小练习 5 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M06 · SECURITY
权限与沙箱:能做什么,在哪里做 审批按钮、规则引擎、容器和操作系统沙箱分别解决什么问题?为什么“问过用户”不等于“隔离了风险”?
先用一个生活比喻 审批像门卫问你有没有预约,沙箱像把访客关在指定房间;前者决定是否放行,后者限制放行后能摸到什么。
这套实现先回答了什么? 先看是否像恶意命令和数据外传,再做额外对抗审查,然后才判断用户是否需要点批准,最后检查重复循环。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 工具检查顺序体现“危险优先” crates/goose/src/agents/agent.rs:659先看是否像恶意命令和数据外传,再做额外对抗审查,然后才判断用户是否需要点批准,最后检查重复循环。 Auto、Approve、SmartApprove 是不同权限语义 crates/goose/src/permission/permission_inspector.rs:159“智能批准”不是无条件执行:读文件一类操作可能自动过,写文件或判断不清的操作仍会问人。 内置开发者工具没有强制工作区边界 crates/goose/src/agents/platform_extensions/developer/shell.rs:25Goose 会先决定“该不该执行”,但一旦放行,命令通常是在真实电脑环境里跑,不是在一个只能碰项目目录的小盒子里。
07 L1 · fact · goose-security-001
工具检查顺序体现“危险优先”
先看源码事实 检查器固定按 Security、Egress、Adversary、Permission、Repetition 顺序注册。
翻译成白话 先看是否像恶意命令和数据外传,再做额外对抗审查,然后才判断用户是否需要点批准,最后检查重复循环。
为什么这对自研重要 审批不是唯一防线;危险检测应在便利性策略之前执行。
边界与风险 具体 inspector 是否生效还受配置控制;例如模式扫描可被关闭。
固定提交源码摘录 复制
659 /// Create a tool inspection manager with default inspectors
660 fn create_tool_inspection_manager(
661 permission_manager: Arc<PermissionManager>,
662 provider: SharedProvider,
663 session_manager: Arc<SessionManager>,
664 ) -> ToolInspectionManager {
665 let mut tool_inspection_manager = ToolInspectionManager::new();
666
667 // Add security inspector (highest priority - runs first)
668 tool_inspection_manager.add_inspector(Box::new(SecurityInspector::new()));
669 tool_inspection_manager.add_inspector(Box::new(EgressInspector::new()));
670
671 // Add adversary inspector (LLM-based review, enabled by ~/.config/goose/adversary.md)
672 tool_inspection_manager.add_inspector(Box::new(AdversaryInspector::new(
673 provider.clone(),
674 session_manager.clone(),
675 )));
676
677 // Add permission inspector (medium-high priority)
678 tool_inspection_manager.add_inspector(Box::new(PermissionInspector::new(
679 permission_manager,
680 provider,
681 session_manager,
682 )));
683
684 // Add repetition inspector (lower priority - basic repetition checking)
685 tool_inspection_manager.add_inspector(Box::new(RepetitionInspector::new(None)));
686
687 tool_inspection_manager
688 }
为什么相信这条结论?查看 1 处证据
08 L1 · fact · goose-permission-001
Auto、Approve、SmartApprove 是不同权限语义
先看源码事实 Auto 模式允许所有调用;Approve/SmartApprove 尊重显式许可,SmartApprove 还会依据工具注解或 LLM 判断只读调用是否可自动放行,未知调用默认要求审批。
翻译成白话 “智能批准”不是无条件执行:读文件一类操作可能自动过,写文件或判断不清的操作仍会问人。
为什么这对自研重要 权限模式必须在 UI 和审计记录中显式呈现,不能用一个笼统的“安全模式”标签。
固定提交源码摘录 复制
159 let action = match goose_mode {
160 GooseMode::Chat => continue,
161 GooseMode::Auto => InspectionAction::Allow,
162 GooseMode::Approve | GooseMode::SmartApprove => {
163 // 1. Check user-defined permission first
164 if let Some(level) = permission_manager.get_user_permission(tool_name) {
165 match level {
166 PermissionLevel::AlwaysAllow => InspectionAction::Allow,
167 PermissionLevel::NeverAllow => InspectionAction::Deny,
168 PermissionLevel::AskBefore => {
169 InspectionAction::RequireApproval(None)
170 }
171 }
172 // 2. Check for a read-only annotation in SmartApprove mode
173 } else if goose_mode == GooseMode::SmartApprove
174 && self.is_readonly_annotated_tool(tool_name)
175 {
176 InspectionAction::Allow
177 // 3. Special case for extension management
178 } else if tool_name == MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE {
179 InspectionAction::RequireApproval(Some(
180 "Extension management requires approval for security".to_string(),
181 ))
182 // 4. Defer to LLM detection (SmartApprove, uncached or legacy cached allow)
183 } else if goose_mode == GooseMode::SmartApprove
… 74 lines omitted; exact range 159–268 …
258 } else {
259 "Tool requires user approval".to_string()
260 },
261 confidence: 1.0, // Permission decisions are definitive
262 inspector_name: self.name().to_string(),
263 finding_id: None,
264 });
265 }
266 }
267
268 Ok(results)
为什么相信这条结论?查看 1 处证据
09 L1 · limitation · goose-sandbox-001
内置开发者工具没有强制工作区边界
先看源码事实 shell 工具最终启动系统 bash/sh/PowerShell;Flatpak 下使用 flatpak-spawn --host。edit 工具解析并接受绝对路径,随后直接 fs::write。
翻译成白话 Goose 会先决定“该不该执行”,但一旦放行,命令通常是在真实电脑环境里跑,不是在一个只能碰项目目录的小盒子里。
为什么这对自研重要 Goose 的默认安全核心是策略检查与人类审批,不是内核级隔离;部署到高风险环境应再叠加容器、VM 或受限执行器。
边界与风险 外层桌面打包、企业运行平台或用户自行容器化可能增加隔离;本结论针对仓库内置 developer shell/edit 的实现。
固定提交源码摘录 复制
25 /// Check if the current process is running inside a Flatpak sandbox.
26 ///
27 /// When inside Flatpak, shell commands must be wrapped with `flatpak-spawn --host`
28 /// to execute on the host system rather than inside the sandbox.
29 #[cfg(not(windows))]
30 pub(crate) fn is_flatpak() -> bool {
31 std::path::Path::new("/.flatpak-info").exists()
32 }
33
34 #[cfg(not(windows))]
35 const FLATPAK_HOST_ARGS: [&str; 2] = ["--host", "--watch-bus"];
36
37 #[cfg(not(windows))]
38 pub(crate) fn flatpak_spawn_command() -> tokio::process::Command {
39 let mut command = tokio::process::Command::new("flatpak-spawn");
40 command.args(FLATPAK_HOST_ARGS);
41 command
42 }
43
44 #[cfg(not(windows))]
45 fn flatpak_spawn_process() -> std::process::Command {
46 let mut command = std::process::Command::new("flatpak-spawn");
47 command.args(FLATPAK_HOST_ARGS);
48 command
49 }
为什么相信这条结论?查看 3 处证据
小练习 6 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M07 · ECOSYSTEM
指令、MCP、Skills 与插件:能力如何接进来 一条系统指令、一个 Skill、一个 MCP server 和一个插件,分别在什么时候进入上下文和执行路径?
先用一个生活比喻 这像给工作室接设备:说明书不是设备,设备也不等于电源;成熟 Harness 会分别治理发现、信任、加载、调用和卸载。
这套实现先回答了什么? 各插件都可能有一个叫 search 的工具,所以 Goose 默认把它们改成“插件名__search”,避免撞名,并缓存工具清单减少重复查询。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 MCP 工具被统一命名空间化、缓存和动态刷新 crates/goose/src/agents/extension_manager.rs:1271各插件都可能有一个叫 search 的工具,所以 Goose 默认把它们改成“插件名__search”,避免撞名,并缓存工具清单减少重复查询。 远端 MCP 支持 HTTP、OAuth 和交互通知 crates/goose/src/agents/extension_manager.rs:612插件不一定是本机子进程,也可以是要登录的远程服务;执行过程中还能弹出“需要用户操作”的中间事件。 MCP UI 元数据采用“先剥离、再可信补写” crates/goose/src/agents/extension_manager.rs:322插件不能只靠自己声称“这是可信 UI”就让前端照单全收,Goose 会清掉敏感标记并由宿主重新验证、重新封装。 系统提示由稳定骨架、扩展说明和路径 hints 组合 crates/goose/src/agents/prompt_manager.rs:95提示词不是一整块写死文本,而是像模板一样拼装;插件能加说明,但会先清理可能伪装成系统标签的字符。
10 L1 · fact · goose-mcp-001
MCP 工具被统一命名空间化、缓存和动态刷新
先看源码事实 ExtensionManager 从所有客户端分页拉取工具,按 extension__tool 前缀公开(特殊配置可不加前缀),缓存聚合结果;扩展增删会提升版本并失效缓存。
翻译成白话 各插件都可能有一个叫 search 的工具,所以 Goose 默认把它们改成“插件名__search”,避免撞名,并缓存工具清单减少重复查询。
为什么这对自研重要 工具注册表既是性能组件也是一致性组件;热插拔后必须原子失效。
固定提交源码摘录 复制
1271 /// Get all tools from all clients with proper prefixing
1272 pub async fn get_prefixed_tools(
1273 &self,
1274 session_id: &str,
1275 extension_name: Option<String>,
1276 ) -> ExtensionResult<Vec<Tool>> {
1277 let all_tools = self.get_all_tools_cached(session_id).await?;
1278 Ok(self.filter_tools(&all_tools, extension_name.as_deref(), None))
1279 }
1280
1281 pub async fn get_prefixed_tools_excluding(
1282 &self,
1283 session_id: &str,
1284 exclude: &str,
1285 ) -> ExtensionResult<Vec<Tool>> {
1286 let all_tools = self.get_all_tools_cached(session_id).await?;
1287 Ok(self.filter_tools(&all_tools, None, Some(exclude)))
1288 }
1289
1290 fn filter_tools(
1291 &self,
1292 tools: &[Tool],
1293 extension_name: Option<&str>,
1294 exclude: Option<&str>,
1295 ) -> Vec<Tool> {
… 36 lines omitted; exact range 1271–1342 …
1332
1333 {
1334 let mut cache = self.tools_cache.lock().await;
1335 let version_after = self.tools_cache_version.load(Ordering::SeqCst);
1336 if version_after == version_before && cache.is_none() {
1337 *cache = Some(Arc::clone(&tools));
1338 }
1339 }
1340
1341 Ok(tools)
1342 }
为什么相信这条结论?查看 2 处证据
11 L1 · fact · goose-mcp-002
远端 MCP 支持 HTTP、OAuth 和交互通知
先看源码事实 扩展可通过 streamable HTTP(Unix 上还可走 socket)连接,复用凭证存储和 OAuth;工具执行可同时返回通知流、ActionRequired 流与最终结果。
翻译成白话 插件不一定是本机子进程,也可以是要登录的远程服务;执行过程中还能弹出“需要用户操作”的中间事件。
为什么这对自研重要 Connector 模型必须同时处理认证生命周期、取消、通知和最终值,不能只把工具当普通函数。
固定提交源码摘录 复制
612 async fn connect_with_auth(
613 auth_manager: rmcp::transport::AuthorizationManager,
614 uri: &str,
615 timeout: Duration,
616 headers: &HashMap<String, String>,
617 provider: SharedProvider,
618 client_name: String,
619 capabilities: GooseMcpClientCapabilities,
620 roots_dir: &std::path::Path,
621 ) -> ExtensionResult<Box<dyn McpClientTrait>> {
622 let mut auth_headers = HeaderMap::new();
623 auth_headers.insert(reqwest::header::USER_AGENT, GOOSE_USER_AGENT);
624 for (key, value) in headers {
625 auth_headers.insert(
626 HeaderName::try_from(key)
627 .map_err(|_| ExtensionError::ConfigError(format!("invalid header: {}", key)))?,
628 value.parse().map_err(|_| {
629 ExtensionError::ConfigError(format!("invalid header value: {}", key))
630 })?,
631 );
632 }
633 #[allow(unused_mut)]
634 let mut auth_client_builder = reqwest::Client::builder().default_headers(auth_headers);
635 #[cfg(target_os = "linux")]
636 {
… 83 lines omitted; exact range 612–730 …
720
721 let transport = StreamableHttpClientTransport::with_client(
722 http_client,
723 StreamableHttpClientTransportConfig::with_uri(uri),
724 );
725
726 // If we have stored OAuth credentials, try refreshing and connecting directly.
727 // This avoids the unnecessary 401 → browser re-auth cycle on every new session.
728 if credential_store.load().await.is_ok_and(|c| c.is_some()) {
729 match oauth_flow(&uri.to_string(), &name.to_string()).await {
730 Ok(auth_manager) => {
为什么相信这条结论?查看 2 处证据
12 L1 · fact · goose-mcp-003
MCP UI 元数据采用“先剥离、再可信补写”
先看源码事实 客户端返回的 goose.mcpApp 和内部工具更新元数据会先被删除;只有宿主主动读取关联资源后才插入可信附件。
翻译成白话 插件不能只靠自己声称“这是可信 UI”就让前端照单全收,Goose 会清掉敏感标记并由宿主重新验证、重新封装。
为什么这对自研重要 所有能改变宿主 UI/行为的扩展元数据都应有信任边界。
固定提交源码摘录 复制
322 fn remove_untrusted_mcp_app_meta(result: &mut CallToolResult) {
323 let Some(meta) = result.meta.as_mut() else {
324 return;
325 };
326
327 meta.0.remove(TRUSTED_TOOL_UPDATE_META_KEY);
328
329 let remove_goose = meta
330 .0
331 .get_mut("goose")
332 .and_then(Value::as_object_mut)
333 .map(|goose_meta| {
334 goose_meta.remove("mcpApp");
335 goose_meta.is_empty()
336 })
337 .unwrap_or(false);
338
339 if remove_goose {
340 meta.0.remove("goose");
341 }
342
343 if meta.0.is_empty() {
344 result.meta = None;
345 }
346 }
为什么相信这条结论?查看 3 处证据
13 L1 · fact · goose-prompt-001
系统提示由稳定骨架、扩展说明和路径 hints 组合
先看源码事实 PromptBuilder 聚合时间、工作目录、扩展说明、可用工具、模式、系统 extras 与 hints;工具排序稳定以利提示缓存,并清洗扩展提供的 Unicode 标签。
翻译成白话 提示词不是一整块写死文本,而是像模板一样拼装;插件能加说明,但会先清理可能伪装成系统标签的字符。
为什么这对自研重要 指令装配应有稳定顺序、来源边界和可测试的净化规则,否则缓存命中与安全都会漂移。
固定提交源码摘录 复制
95 pub fn with_hints(mut self, working_dir: &Path) -> Self {
96 let hints_filenames = get_context_filenames();
97 let ignore_patterns = build_gitignore(working_dir);
98
99 let hints = load_hint_files(working_dir, &hints_filenames, &ignore_patterns);
100
101 if !hints.is_empty() {
102 self.hints = Some(hints);
103 }
104 self
105 }
106
107 pub fn with_enable_subagents(mut self, subagents_enabled: bool) -> Self {
108 self.subagents_enabled = subagents_enabled;
109 self
110 }
111
112 pub fn with_goose_mode(mut self, mode: GooseMode) -> Self {
113 self.goose_mode = Some(mode);
114 self
115 }
116
117 pub fn build(self) -> String {
118 let mut extensions_info = self.extensions_info;
119
… 35 lines omitted; exact range 95–165 …
155 max_tools: MAX_TOOLS,
156 code_execution_mode: self.code_execution_mode,
157 moim_system_prompt_block: moim::system_prompt_block(),
158 };
159
160 let base_prompt = if let Some(override_prompt) = &self.manager.system_prompt_override {
161 let sanitized_override_prompt = sanitize_unicode_tags(override_prompt);
162 prompt_template::render_string(&sanitized_override_prompt, &context)
163 } else {
164 prompt_template::render_template("system.md", &context)
165 }
为什么相信这条结论?查看 3 处证据
14 L1 · fact · goose-skills-001
Skills 采用渐进加载和多生态目录兼容
先看源码事实 项目级 .agents/.goose/.claude 目录先于全局目录发现;系统提示只列技能名与描述,真正调用时才载入 SKILL.md 或支持文件,并校验支持文件解析后仍位于技能目录内。
翻译成白话 模型一开始只拿到“技能目录”,需要时再打开说明书和附件,既省上下文,也防止通过 ../ 偷读技能目录外文件。
为什么这对自研重要 技能系统应把发现、选择、加载分开,并对支持文件做 canonical path 校验。
固定提交源码摘录 复制
313 /// Every directory the agent reads skills from, paired with whether each is a
314 /// global (home-rooted) location. Order matches discovery precedence: project
315 /// dirs first, then global dirs.
316 pub fn all_skill_dirs(working_dir: Option<&Path>) -> Vec<(PathBuf, bool)> {
317 let mut dirs: Vec<(PathBuf, bool)> = Vec::new();
318
319 if let Some(wd) = working_dir {
320 dirs.push((wd.join(".agents").join("skills"), false));
321 dirs.push((wd.join(".goose").join("skills"), false));
322 dirs.push((wd.join(".claude").join("skills"), false));
323 }
324
325 let home = dirs::home_dir();
326 if let Some(h) = home.as_ref() {
327 dirs.push((h.join(".agents").join("skills"), true));
328 }
329 dirs.push((Paths::config_dir().join("skills"), true));
330 if let Some(h) = home.as_ref() {
331 dirs.push((h.join(".claude").join("skills"), true));
332 dirs.push((h.join(".config").join("agents").join("skills"), true));
333 }
334
335 dirs.extend(
336 installed_plugin_skill_dirs()
337 .into_iter()
338 .map(|dir| (dir, true)),
339 );
340
341 dirs
为什么相信这条结论?查看 3 处证据
15 L1 · fact · goose-hooks-001
插件 Hooks 覆盖工具、文件、Shell、会话和停止事件
先看源码事实 HookManager 从启用插件的 hooks/hooks.json 加载命令动作;PreToolUse 和 Stop 可阻塞,其他 hook 失败默认放行;Stop 阻塞另有连续次数上限。
翻译成白话 组织可以在“运行命令前”“改文件后”“会话开始”“Agent 想结束”等时点执行自己的脚本,但普通通知脚本坏掉不会让整个 Agent 瘫痪。
为什么这对自研重要 Hook 的失败语义必须逐事件定义;治理门可以 fail-closed,旁路观测更适合 fail-open。
固定提交源码摘录 复制
64 impl HookEvent {
65 fn name(&self) -> &'static str {
66 match self {
67 HookEvent::PreToolUse => "PreToolUse",
68 HookEvent::PostToolUse => "PostToolUse",
69 HookEvent::PostToolUseFailure => "PostToolUseFailure",
70 HookEvent::SessionStart => "SessionStart",
71 HookEvent::SessionEnd => "SessionEnd",
72 HookEvent::UserPromptSubmit => "UserPromptSubmit",
73 HookEvent::BeforeReadFile => "BeforeReadFile",
74 HookEvent::AfterFileEdit => "AfterFileEdit",
75 HookEvent::BeforeShellExecution => "BeforeShellExecution",
76 HookEvent::AfterShellExecution => "AfterShellExecution",
77 HookEvent::Stop => "Stop",
78 }
79 }
80
81 fn from_name(name: &str) -> Option<Self> {
82 Some(match name {
83 "PreToolUse" => HookEvent::PreToolUse,
84 "PostToolUse" => HookEvent::PostToolUse,
85 "PostToolUseFailure" => HookEvent::PostToolUseFailure,
86 "SessionStart" => HookEvent::SessionStart,
87 "SessionEnd" => HookEvent::SessionEnd,
88 "UserPromptSubmit" => HookEvent::UserPromptSubmit,
89 "BeforeReadFile" => HookEvent::BeforeReadFile,
90 "AfterFileEdit" => HookEvent::AfterFileEdit,
91 "BeforeShellExecution" => HookEvent::BeforeShellExecution,
92 "AfterShellExecution" => HookEvent::AfterShellExecution,
93 "Stop" => HookEvent::Stop,
94 _ => return None,
95 })
为什么相信这条结论?查看 3 处证据
小练习 7 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M08 · COLLABORATION
子 Agent:把一个大任务拆成可治理的协作 什么时候是普通工具调用,什么时候才算子 Agent?子 Agent 的上下文、预算、取消和结果怎样回到父 Agent?
先用一个生活比喻 不是把同事叫来聊天就叫协作;真正的协作要有工单、权限、截止时间、交付物和回收机制。
这套实现先回答了什么? 主 Agent 真正启动了另一个有独立历史和工具集的执行循环,而不是在同一段对话里假装分身;但只允许一层委派。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 子 Agent 是独立 Agent 与子会话,不是主提示词里的角色扮演 crates/goose/src/agents/subagent_handler.rs:121主 Agent 真正启动了另一个有独立历史和工具集的执行循环,而不是在同一段对话里假装分身;但只允许一层委派。
16 L1 · fact · goose-subagent-001
子 Agent 是独立 Agent 与子会话,不是主提示词里的角色扮演
先看源码事实 SubagentHandler 新建 Agent,继承/覆盖 provider 与 model,按配置装载扩展和响应 schema,创建 SubAgent 类型会话,并运行标准 Agent.reply;子 Agent 系统提示明确禁止继续创建子 Agent。
翻译成白话 主 Agent 真正启动了另一个有独立历史和工具集的执行循环,而不是在同一段对话里假装分身;但只允许一层委派。
为什么这对自研重要 独立会话便于隔离上下文、统计成本和回传结果;禁止递归可控制爆炸式 fan-out。
固定提交源码摘录 复制
121 fn get_agent_messages(params: SubagentRunParams) -> AgentMessagesFuture {
122 Box::pin(async move {
123 let SubagentRunParams {
124 config,
125 recipe,
126 task_config,
127 session_id,
128 cancellation_token,
129 on_message,
130 notification_tx,
131 ..
132 } = params;
133
134 let system_instructions = recipe.instructions.clone().unwrap_or_default();
135 let user_task = recipe
136 .prompt
137 .clone()
138 .unwrap_or_else(|| "Begin.".to_string());
139
140 let agent = Arc::new(Agent::with_config(config));
141
142 agent
143 .update_provider(
144 task_config.provider.clone(),
145 task_config.model_config.clone(),
… 74 lines omitted; exact range 121–230 …
220 Err(e) => {
221 tracing::error!("Error receiving message from subagent: {}", e);
222 break;
223 }
224 }
225 }
226
227 let final_output = get_final_output(&agent, has_response_schema).await;
228
229 Ok((conversation, final_output))
230 })
为什么相信这条结论?查看 2 处证据
小练习 8 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M09 · STATE
会话、持久化与观测:让一次运行变成可追溯事实 如果进程崩了、用户刷新了、任务跑了一夜,系统凭什么恢复并解释“刚才究竟发生了什么”?
先用一个生活比喻 内存像白板,数据库像目录,append-only journal 像监控录像;可靠 Harness 不只保存最后答案,还保存每次转弯。
这套实现先回答了什么? 对话不只是屏幕上的临时文本:每条消息、用的模型、父子会话、花费和压缩前后 token 都能落盘追踪。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
源码问题 固定提交给出的线索 白话结论 会话、消息、成本与压缩指标落到 SQLite/WAL crates/goose/src/session/session_manager.rs:45对话不只是屏幕上的临时文本:每条消息、用的模型、父子会话、花费和压缩前后 token 都能落盘追踪。 Tracing 以 goose:: span 生成 trace/span 观测事件 crates/goose/src/tracing/observation_layer.rs:103内部关键步骤会形成一棵调用轨迹,不是只打一长串平面日志;同时避免把所有依赖库噪声都收进来。
17 L1 · fact · goose-session-001
会话、消息、成本与压缩指标落到 SQLite/WAL
先看源码事实 SessionManager 使用 SQLite、WAL 和迁移管理持久化 sessions、messages、usage_ledger;会话记录 provider/model/mode/project/parent,usage ledger 区分输入输出、缓存、总成本与压缩后保留 token。
翻译成白话 对话不只是屏幕上的临时文本:每条消息、用的模型、父子会话、花费和压缩前后 token 都能落盘追踪。
为什么这对自研重要 成本与上下文恢复应成为一等数据模型,而不是散落在日志字符串里。
固定提交源码摘录 复制
45 pub enum SessionType {
46 #[default]
47 User,
48 Scheduled,
49 SubAgent,
50 Hidden,
51 Terminal,
52 Gateway,
53 Acp,
54 }
55
56 static SESSION_STORAGE: LazyLock<Arc<SessionStorage>> =
57 LazyLock::new(|| Arc::new(SessionStorage::new(Paths::data_dir())));
58
59 #[derive(Debug, Clone, Serialize, Deserialize)]
60 pub struct Session {
61 pub id: String,
62 pub working_dir: PathBuf,
63 #[serde(alias = "description")]
64 pub name: String,
65 #[serde(default)]
66 pub user_set_name: bool,
67 #[serde(default)]
68 pub session_type: SessionType,
69 pub created_at: DateTime<Utc>,
… 16 lines omitted; exact range 45–96 …
86 #[serde(default)]
87 pub goose_mode: GooseMode,
88 #[serde(default)]
89 pub archived_at: Option<DateTime<Utc>>,
90 #[serde(default)]
91 pub project_id: Option<String>,
92 #[serde(default)]
93 pub parent_session_id: Option<String>,
94 #[serde(default)]
95 pub last_message_snippet: Option<String>,
96 }
为什么相信这条结论?查看 3 处证据
18 L1 · fact · goose-observe-001
Tracing 以 goose:: span 生成 trace/span 观测事件
先看源码事实 ObservationLayer 仅处理 target 以 goose:: 开头的 tracing 事件,跟踪 span 层级、输入输出和批次,并可把 reply 最终文本记录为 trace_output。
翻译成白话 内部关键步骤会形成一棵调用轨迹,不是只打一长串平面日志;同时避免把所有依赖库噪声都收进来。
为什么这对自研重要 观测层和业务层通过结构化 span 字段连接,适合接入 Langfuse 等后端,但敏感输出治理仍需部署方关注。
固定提交源码摘录 复制
103 impl ObservationLayer {
104 pub async fn handle_span(&self, span_id: u64, span_data: SpanData) {
105 let observation_id = span_data.observation_id.clone();
106
107 {
108 let mut spans = self.span_tracker.lock().await;
109 spans.add_span(span_id, observation_id.clone());
110 }
111
112 // Get parent ID if it exists
113 let parent_id = if let Some(parent_span_id) = span_data.parent_span_id {
114 let spans = self.span_tracker.lock().await;
115 spans.get_span(parent_span_id).cloned()
116 } else {
117 None
118 };
119
120 let trace_id = self.ensure_trace_id().await;
121
122 // Create the span observation
123 let mut batch = self.batch_manager.lock().await;
124 batch.add_event(
125 "observation-create",
126 json!({
127 "id": observation_id,
… 46 lines omitted; exact range 103–184 …
174 "name": Utc::now().timestamp().to_string(),
175 "timestamp": Utc::now().to_rfc3339(),
176 "input": {},
177 "metadata": {},
178 "tags": [],
179 "public": false
180 }),
181 );
182
183 trace_id
184 }
为什么相信这条结论?查看 3 处证据
小练习 9 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M10 · ENGINEERING
测试、恢复与工程取舍:把漂亮机制变成可靠产品 哪些行为有测试证明?哪些只是配置或 prompt 约定?当安全、性能、可恢复性冲突时,源码选择了什么?
先用一个生活比喻 这像验收一座桥:图纸说明结构,测试证明承重,故障演练证明断电后还能不能让人安全回来。
本节阅读法 先问问题 → 读事实 → 看代码 → 做迁移判断
读源码时先问 本课的判断方式 这一层有没有独立证据? 没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。 谁拥有最终控制权? 区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念 固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。
小练习 10 打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。
M11 · PRACTICE
把读懂变成会判断 下面的练习不要求你先写一个完整 Agent,而是训练你检查设计边界:事实是什么、推断是什么、如果换成自研产品要补哪一层。
Q1 单一流式 Agent 循环驱动推理、工具和持久化 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 Harness 的真正核心是状态机而不是提示词;重试、转向、停止钩子和上下文恢复都进入同一控制环。
证据:crates/goose/src/agents/agent.rs:1930 Q2 结束条件有防失控上限 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 所有“让 Agent 继续”的机制都需要配额和最终逃生门。
证据:crates/goose/src/agents/agent.rs:67 Q3 Provider 以流式协议统一,工具调用必须完整再上送 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 Provider 适配层承担格式差异,Agent 主循环不需要为每家 API 复制控制逻辑。
证据:crates/goose-provider-types/src/base.rs:281 Q4 80% 阈值触发结构化压缩 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 压缩的目标不是“越短越好”,而是保留任务状态、文件改动、错误、待办和下一步。
证据:crates/goose/src/context_mgmt/mod.rs:26 Q5 上下文超限采用渐进式工具结果剥离和一次恢复重试 问题: 如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?
参考答案 Harness 应区分账单 token、保留 token 和可牺牲的工具噪声,并给恢复动作设次数上限。
证据:crates/goose/src/context_mgmt/mod.rs:319
APPENDIX · SOURCE INDEX
本课读过的实现文件 文件索引帮助你在课程外继续追踪调用链;每一条路径来自固定提交的证据账本。
01 crates/goose/src/agents/agent.rsL1930–2043, L2883–2929, L67–79, L2773–2793, L2892–2915, L2533–2591, L2210–2265, L2267–2337, L659–688, L2922–2924 02 crates/goose-provider-types/src/base.rsL281–286, L394–427 03 crates/goose/src/context_mgmt/mod.rsL26–49, L78–188, L319–398 04 crates/goose/src/context_mgmt/structured.rsL13–38 05 crates/goose/src/permission/permission_inspector.rsL159–268 06 crates/goose/src/agents/platform_extensions/developer/shell.rsL25–49, L633–700 07 crates/goose/src/agents/platform_extensions/developer/edit.rsL47–105 08 crates/goose/src/agents/extension_manager.rsL1271–1342, L1391–1455, L612–730, L1786–1895, L322–346, L1866–1880, L2881–2904 09 crates/goose/src/agents/prompt_manager.rsL95–165, L170–273, L289–360 10 crates/goose/src/skills/mod.rsL313–341 11 crates/goose/src/skills/client.rsL132–187, L245–266 12 crates/goose/src/hooks/mod.rsL64–95, L237–284, L292–425 13 crates/goose/src/agents/subagent_handler.rsL121–230 14 crates/goose/src/prompts/subagent_system.mdL1–38 15 crates/goose/src/session/session_manager.rsL45–96, L843–890, L893–1031 16 crates/goose/src/tracing/observation_layer.rsL103–184, L196–336
下一步 从课程回到报告,做一次反向核验 教程负责让你读懂,报告负责让你查证。打开报告页,任选一个章节,尝试只靠源码摘录复述它的边界。
查看 Goose 报告 ↗