goose/source-notesupstream ↗
goose · version 1.45.0 · Rust 1.94.1

第 2 章:Agent Loop——goose 的心跳

2.1 是什么:一个“流式回复 + 工具回合”的状态机

goose 的 Agent Loop 不是简单的 while model.has_tool_call()。它还要处理:用户 slash command、自动压缩、流式 delta、工具审批、前端工具、MCP 通知、取消、重试、goal/grind、recipe retry、stop hook 和空响应恢复。因此 Agent::reply 返回的是 BoxStream<Result<AgentEvent>>,而不是一个最终字符串。

stateDiagram-v2
  [*] --> Prepare
  Prepare --> Command: slash command
  Command --> [*]: 只修改历史 / 立即返回
  Prepare --> Compact: 超过 context threshold
  Compact --> Prepare
  Prepare --> Model
  Model --> Emit: 文本 / thinking delta
  Model --> Inspect: tool request
  Inspect --> Approve: 需要确认
  Inspect --> Dispatch: 可直接执行
  Approve --> Dispatch: Allow
  Approve --> Model: Deny / tool response
  Dispatch --> ToolEvents: result / notification / action-required
  ToolEvents --> Model: 继续当前 loop
  Model --> Stop: 没有 tool call
  Stop --> Model: goal / grind / retry / steer 未完成
  Stop --> Compact: recovery compact
  Stop --> [*]: stop hook 允许退出

2.2 源码怎么做:三层函数把准备、编排和循环分开

reply:唯一的事件身份边界

Agent::reply 调用 reply_impl,再通过 ensure_message_event_id 为同一个逻辑消息的多个事件补齐一致 ID。这一层很关键:CLI、桌面端、ACP 都可以把文本 delta 合并,而不需要重新猜测哪个事件属于哪条消息。

reply_impl:回合前的卫生处理

它先为用户消息补 ID,处理 ActionRequired 的 elicitation response,然后读取 session。接着按顺序处理:

  1. 不可见输入直接入库但不启动 Agent。
  2. 首轮触发 SessionStart hook;用户 prompt 触发 UserPromptSubmit hook。
  3. 运行 slash command;其中 goal/grind 命令会写入可见确认,再注入 agent-only kickoff,让 Agent 立刻开始工作。
  4. 调用 check_if_compaction_needed,必要时先完成压缩并发出 HistoryReplaced
  5. 进入 reply_internal

reply_internal:真正的循环

它先通过 prepare_reply_context 拿到 conversation、tools、toolshim tools、system prompt、model config。循环体中维护 turns_takenmax_turns、压缩次数、空回合重试、待处理 steer、stop-hook 连续阻断次数等状态。

模型输出被拆成两条路径:

工具执行通过 tool_stream 把三个来源合并成一个流:MCP server notification、action-required 消息、最终工具结果。这样“工具正在发进度”和“工具最终返回”可以被同一个回合消费。

2.3 为什么这样做:不把“停止”当成单一条件

最有价值的设计是退出逻辑。没有工具调用并不等于马上结束:

这是一种“显式状态 + 可观测事件”的 runtime 思路:每个继续条件都变成历史中的消息或事件,后续 provider 看到的是一个可解释的上下文,而不是藏在宿主里的魔法状态。

2.4 一次工具回合的伪代码

loop {
    response = provider.stream(system, visible_history, tools).await?;
    emit_text_and_thinking(response.deltas);

    if response.has_tool_requests() {
        inspections = inspect_tools(requests, history);
        approved = route_permissions(inspections);
        results = dispatch(approved).await;
        persist(requests, results);
        continue;
    }

    if needs_goal_retry_or_recipe_retry_or_compaction() {
        inject_continuation();
        continue;
    }
    break_if_stop_hook_allows();
}

源码定位