Harness · Coding Agent Book01 / Goose
研究总览
M01 · SOURCE-GROUNDED TUTORIAL

Goose
从源码学会它怎么工作

MCP 原生、扩展友好、单循环清楚;安全边界更依赖工具检查而非 OS 沙箱。 我们不把 README 当结论,而是沿主循环、工具、上下文、权限、扩展、协作和状态一路读到实现。

Rust · MCP-first 通用 AgentApache-2.0021b0db8dbee18 个结论 · 43 处引用
这门课怎么读

先建立直觉,再沿一条任务链钻进代码

参考教程的做法不是把 API 名称罗列出来,而是从一个小白能理解的问题开始,先解释“为什么需要这个机制”,再用概念对比、执行链路和固定提交的源码回答“它究竟怎么做”。本页把 Goose 的 18 个源码结论重新编排成十节课;每个结论都保留证据等级、文件路径、行号和可点击源码。

你会得到一张可复述的架构地图一次完整任务的链路追踪能迁移到自研 Harness 的设计判断
你不会得到把 README 功能当成已验证事实把 prompt 约束说成 OS 沙箱把一次双模型调用夸成多 Agent 平台
M00 · MAP

先看全景:这个 Agent 的控制面在哪里

下面的图不是产品宣传图,而是把固定提交里最关键的入口、循环、模型、工具、安全、状态和协作节点放在一张地图上。

架构图全屏打开 ↗
一轮执行链路全屏打开 ↗
核心机制

单一流式循环;完整 tool call 后并发;防失控上限

上下文

80% 结构化压缩;超窗逐级剥离工具结果

适用建设

重连接器、桌面交互、企业工具集成

M00.5 · TRACE

跟踪一个任务:从输入到交付

把下面九步当成你读源码时的“地图坐标”。每到一个节点,都能回到后面的章节查具体实现。

读图提醒

箭头只表示控制面之间的关系,不代表每个实现都同步、串行或拥有 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

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M04 · TOOLS

工具系统:Agent 的手脚怎样被注册和调度

模型看见的工具说明,和真正执行工具的代码,是不是同一个东西?并发、编辑和失败结果怎么处理?

先用一个生活比喻

工具系统像机场:模型提交登机牌,注册表确认航班,权限闸机检查证件,调度器决定跑道,最后才允许真正起飞。

这套实现先回答了什么?

模型一次要读三个文件时,不必傻等第一个完成再做第二个;但每个调用先过安全和审批门。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
同一模型 turn 的多个工具经检查后并发执行crates/goose/src/agents/agent.rs:2210模型一次要读三个文件时,不必傻等第一个完成再做第二个;但每个调用先过安全和审批门。
04
L1 · fact · goose-tools-001

同一模型 turn 的多个工具经检查后并发执行

先看源码事实

工具请求先统一通过 inspector 和许可分组;已批准及用户批准的工具流随后用 stream::select_all 合并,结果按 request_id 配回各自响应。

翻译成白话

模型一次要读三个文件时,不必傻等第一个完成再做第二个;但每个调用先过安全和审批门。

为什么这对自研重要

并发提高吞吐,但必须用稳定 ID 做结果配对,并允许单个工具发通知、请求交互或取消。

固定提交源码摘录
 2210                                      // Run all tool inspectors
 2211                                      let inspection_results = self.tool_inspection_manager
 2212                                          .inspect_tools(
 2213                                              &session_config.id,
 2214                                              &remaining_requests,
 2215                                              conversation.messages(),
 2216                                              goose_mode,
 2217                                          )
 2218                                          .await?;
 2219  
 2220                                      let permission_check_result = self.tool_inspection_manager
 2221                                          .process_inspection_results_with_permission_inspector(
 2222                                              &remaining_requests,
 2223                                              &inspection_results,
 2224                                          )
 2225                                          .unwrap_or_else(|| {
 2226                                              let mut result = PermissionCheckResult {
 2227                                                  approved: vec![],
 2228                                                  needs_approval: vec![],
 2229                                                  denied: vec![],
 2230                                              };
 2231                                              result.needs_approval.extend(remaining_requests.iter().cloned());
 2232                                              result
 2233                                          });
 2234  
      … 20 lines omitted; exact range 2210–2265 …
 2255                                              &mut tool_futures,
 2256                                              &mut request_to_response_map,
 2257                                              cancel_token.clone(),
 2258                                              &session,
 2259                                              &inspection_results,
 2260                                          );
 2261  
 2262                                          while let Some(msg) = tool_approval_stream.try_next().await? {
 2263                                              yield AgentEvent::Message(msg);
 2264                                          }
 2265                                      }
为什么相信这条结论?查看 2 处证据
小练习 4

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

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/WALcrates/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

本课读过的实现文件

文件索引帮助你在课程外继续追踪调用链;每一条路径来自固定提交的证据账本。

  1. 01crates/goose/src/agents/agent.rsL1930–2043, L2883–2929, L67–79, L2773–2793, L2892–2915, L2533–2591, L2210–2265, L2267–2337, L659–688, L2922–2924
  2. 02crates/goose-provider-types/src/base.rsL281–286, L394–427
  3. 03crates/goose/src/context_mgmt/mod.rsL26–49, L78–188, L319–398
  4. 04crates/goose/src/context_mgmt/structured.rsL13–38
  5. 05crates/goose/src/permission/permission_inspector.rsL159–268
  6. 06crates/goose/src/agents/platform_extensions/developer/shell.rsL25–49, L633–700
  7. 07crates/goose/src/agents/platform_extensions/developer/edit.rsL47–105
  8. 08crates/goose/src/agents/extension_manager.rsL1271–1342, L1391–1455, L612–730, L1786–1895, L322–346, L1866–1880, L2881–2904
  9. 09crates/goose/src/agents/prompt_manager.rsL95–165, L170–273, L289–360
  10. 10crates/goose/src/skills/mod.rsL313–341
  11. 11crates/goose/src/skills/client.rsL132–187, L245–266
  12. 12crates/goose/src/hooks/mod.rsL64–95, L237–284, L292–425
  13. 13crates/goose/src/agents/subagent_handler.rsL121–230
  14. 14crates/goose/src/prompts/subagent_system.mdL1–38
  15. 15crates/goose/src/session/session_manager.rsL45–96, L843–890, L893–1031
  16. 16crates/goose/src/tracing/observation_layer.rsL103–184, L196–336
下一步

从课程回到报告,做一次反向核验

教程负责让你读懂,报告负责让你查证。打开报告页,任选一个章节,尝试只靠源码摘录复述它的边界。

查看 Goose 报告 ↗