AgentLoop 状态机
对应源码
| 主题 | 源码 |
|---|---|
| 状态、工作对象和转换表 | nanobot/agent/loop.py::TurnState、TurnContext、AgentLoop._TRANSITIONS |
| 状态机驱动器 | nanobot/agent/loop.py::AgentLoop._process_message |
| 各阶段处理器 | nanobot/agent/loop.py::AgentLoop._state_restore 到 AgentLoop._state_respond |
| Loop 与 Runner 交界 | nanobot/agent/loop.py::AgentLoop._run_agent_loop、nanobot/agent/runner.py::AgentRunSpec |
学习目标
读完后,你应当能够:
- 解释八个
TurnState; - 找到状态转换表和状态 handler;
- 说明普通消息与 slash command 为什么走不同路径;
- 说明 AgentRunner 在整个 Turn 中只负责哪一段。
前置知识
- 理解 Session Key;
- 能读 enum、dataclass 和异步函数;
- 已阅读 消息、Session Key 与入口。
1. TurnContext 是一次 Turn 的工作台
定义:
nanobot/agent/loop.py::TurnContext
它将一次 Turn 中逐步产生的数据放在同一个对象上:
| 阶段 | 典型字段 |
|---|---|
| 入口 | msg、session_key、turn_id |
| Session | session、history、pending_summary |
| Context | initial_messages |
| Runner 结果 | final_content、tools_used、all_messages、stop_reason |
| 流式和进度 | on_stream、on_progress、on_retry_wait |
| 注入 | pending_queue、had_injections |
| 输出 | outbound、suppress_response |
| 诊断 | turn_latency_ms、trace |
它是运行时对象,不是直接写入磁盘的持久化 schema。
2. 状态转换表
AgentLoop._TRANSITIONS 显式定义:
stateDiagram-v2 [*] --> RESTORE RESTORE --> COMPACT: ok COMPACT --> COMMAND: ok COMMAND --> BUILD: dispatch COMMAND --> DONE: shortcut BUILD --> RUN: ok RUN --> SAVE: ok SAVE --> RESPOND: ok RESPOND --> DONE: ok DONE --> [*]
handler 返回事件字符串,driver 根据 (当前状态, 事件) 查找下一状态。
DONE 是终止标记,不会执行 _state_done()。因此普通主路径会经过八个枚举状态,但 ctx.trace 只记录前七个实际 handler;shortcut command 的 trace 更短。
这种设计的好处是:
- 状态边界可观察;
- shortcut 清晰;
- 每个阶段的职责相对集中;
- trace 可以记录耗时和错误。
3. _process_message() 驱动状态机
主要步骤:
- 计算 Session;
- 创建
TurnContext,初始状态为RESTORE; - 根据当前状态找到 handler;
- 执行 handler;
- 记录 state trace;
- 使用转换表进入下一状态;
- 到
DONE后返回 outbound 或结果。
对应的驱动骨架可以直接从 AgentLoop._process_message() 读出:
while ctx.state is not TurnState.DONE:
handler_name = f"_state_{ctx.state.name.lower()}"
handler = getattr(self, handler_name, None)
if handler is None:
raise RuntimeError(f"Missing state handler for {ctx.state}")
event = await handler(ctx)
next_state = self._TRANSITIONS.get((ctx.state, event))
if next_state is None:
raise RuntimeError(
f"[turn {ctx.turn_id}] No transition from {ctx.state} "
f"on event {event!r}"
)
ctx.state = next_state这是删去 trace 计时和日志后的源码节选。它揭示了两个约束:新增枚举状态时必须有同名 _state_<name>() handler;handler 新增事件时必须同步增加转换表项,否则会明确失败,而不是悄悄走默认分支。
调试一次 Turn 时,优先在 _process_message() 的状态循环观察:
ctx.state- handler 返回的 event
ctx.trace- 哪个状态首次出现异常
4. RESTORE:恢复和准备入站
handler:
AgentLoop._state_restore
主要职责:
- 处理媒体或文档引用;
- 确保 Session 存在;
- 发布
SessionTurnStarted; - 持久化消息的 workspace scope;
- 恢复 runtime checkpoint;
- 恢复 pending user turn。
这里解决的是“上一轮可能留下什么,以及当前消息属于哪个运行范围”,还没有调用模型。
5. COMPACT:处理过期或待摘要 Session
handler:
AgentLoop._state_compact
调用:
AutoCompact.prepare_session
它可能返回更新后的 Session 和 pending summary。真正按 token 触发的 consolidation 还会在 BUILD 发生。
两种压缩不要混为一谈
AutoCompact、Consolidator、Runner 的 microcompact/tool-result budget 作用层级不同。后续“状态与长期运行”会分别解释。
6. COMMAND:先处理 slash command
handler:
AgentLoop._state_command
它构造 CommandContext,交给命令路由。
如果命令返回 OutboundMessage:
- event 为
shortcut; - 跳过 BUILD、RUN、SAVE、RESPOND;
- 直接进入 DONE;
- 但为了 WebUI 能在 Turn 结束后看到历史,普通命令仍会显式保存 user/assistant command 消息;
/new是特例,因为它有意清空 Session。
这说明:
shortcut 跳过通用保存状态,不等于完全不持久化。
7. BUILD:构造本轮运行输入
handler:
AgentLoop._state_build
主要职责:
- 非 ephemeral Turn 按 token 检查 consolidation;
- 设置当前 ToolContext;
- 重置 MessageTool 的本轮状态;
- 从 Session 获取受预算约束的 history;
- 记录当前 Provider/model runtime;
- 由 ContextBuilder 构造
initial_messages; - 提前持久化当前 user 消息;
- 创建 progress 和 retry-wait callback。
这里有一个重要事实:
当前 user 消息在 Runner 完成前就可能被提前持久化。
这样做配合 pending user turn 和 checkpoint,避免长 Turn 或中断时完全丢失用户输入。
8. RUN:调用 AgentRunner
handler:
AgentLoop._state_run
主要职责:
- 记录可见运行开始时间;
- 发布
"running"状态; - 调用
_run_agent_loop(); - 接收 final content、tools used、完整运行消息、stop reason 和 injection 标记;
- 处理可能的 continuation。
_run_agent_loop() 将当前配置转换成 AgentRunSpec,包括:
- initial messages;
- ToolRegistry;
- model;
- iteration 限制;
- tool result 限制;
- timeout;
- stream/progress callback;
- checkpoint/injection callback;
- goal predicate;
- finalization policy。
9. SAVE:把运行结果落到 Session
handler:
AgentLoop._state_save
主要职责:
- 处理 continuation 保存边界;
- 为不应为空的响应提供统一空响应提示;
- 计算 latency;
- 保存本轮新增消息;
- 记录 runtime latency;
- 执行 Session 文件上限处理;
- 后台安排 consolidation;
- 清理 pending user turn;
- 清理 runtime checkpoint;
- 保存 Session。
10. RESPOND:构造对外回复
handler:
AgentLoop._state_respond
如果本轮被设置为 suppress response,则不创建 OutboundMessage。否则调用 _assemble_outbound():
- 选择 channel 和 chat_id;
- 使用 final content;
- 标记是否已 stream;
- 添加 latency;
- 考虑 MessageTool 已经主动发送消息时是否抑制重复回复。
一次普通 Turn 的数据变化
flowchart TD M["InboundMessage"] --> R["RESTORE<br/>Session + checkpoint"] R --> C["COMPACT<br/>pending summary"] C --> CMD["COMMAND<br/>普通消息继续"] CMD --> B["BUILD<br/>history + initial_messages<br/>提前保存 user"] B --> RUN["RUN<br/>AgentRunResult"] RUN --> S["SAVE<br/>追加 assistant/tool<br/>清 checkpoint"] S --> O["RESPOND<br/>OutboundMessage"]
观察任务
在 _process_message() 的状态循环记录:
| state | 观察变量 |
|---|---|
| RESTORE | ctx.session.metadata |
| COMPACT | ctx.pending_summary |
| COMMAND | event 是 dispatch 还是 shortcut |
| BUILD | ctx.history、ctx.initial_messages |
| RUN | ctx.stop_reason、ctx.tools_used |
| SAVE | Session 新增了哪些消息 |
| RESPOND | ctx.outbound.metadata |
分别发送普通消息和 /help,比较经过的状态。
源码与测试
nanobot/agent/loop.pyTurnStateTurnContext_TRANSITIONS_process_message_state_*
tests/agent/test_loop_runner_integration.pytests/agent/test_loop_save_turn.pytests/agent/test_stop_preserves_context.pytests/command/test_router_dispatchable.py
自检
- 为什么 COMMAND 在 BUILD 之前?
- 为什么 shortcut command 仍可能保存历史?
- 为什么 user 消息会在 Runner 完成前提前持久化?
- AgentRunner 的结果在哪个状态进入 Session?
- MessageTool 已发送消息时,RESPOND 为什么可能不再返回普通回复?