保存、响应与 Runtime Event
对应源码
| 主题 | 源码 |
|---|---|
| 保存边界 | nanobot/agent/loop.py::AgentLoop._save_turn、AgentLoop._state_save |
| 最终响应 | nanobot/agent/loop.py::AgentLoop._state_respond、AgentLoop._assemble_outbound |
| Session JSONL | nanobot/session/manager.py::Session、SessionManager.save |
| 出站消息 | nanobot/bus/events.py::OutboundMessage、nanobot/bus/queue.py::MessageBus |
| 运行事件 | nanobot/bus/runtime_events.py::RuntimeEventBus、RuntimeEventPublisher |
| WebUI Turn 协调 | nanobot/session/webui_turns.py |
学习目标
读完后,你应当能够:
- 说明 Runner 结果如何写入 Session;
- 区分 OutboundMessage 与 Runtime Event;
- 解释 MessageTool 抑制重复回复的行为;
- 找到 Turn latency、checkpoint 清理和完成事件的位置。
前置知识
- 已理解 AgentLoop 状态机;
- 已理解
AgentRunResult; - 知道 Session 是持久化状态。
1. Runner 返回的 messages 不能全部盲目保存
AgentRunner 返回:
- 最终文本;
- 运行中累积 messages;
- tools used;
- stop reason;
- usage 等。
AgentLoop _save_turn() 负责把本轮应持久化的增量加入 Session,并执行:
- 内容清理;
- 孤立工具结果处理;
- tool output 限制;
- provider-specific block 清理;
- 本轮保存边界计算。
原因:
- Runner 为模型请求可能生成临时修复;
- 某些 provider-specific 字段不适合持久化;
- 超大 tool result 会污染未来上下文和磁盘;
- user 消息可能已经在 BUILD 提前保存。
2. SAVE 阶段
AgentLoop._state_save() 顺序:
flowchart TD A["Continuation 保存边界"] --> B["处理空 final content"] B --> C["计算 latency"] C --> D["_save_turn"] D --> E["记录 runtime latency"] E --> F["Session file cap / archive"] F --> G["安排后台 consolidation"] G --> H["清 pending user turn"] H --> I["清 runtime checkpoint"] I --> J["SessionManager.save"]
这里是“模型运行结果”转换成“长期可恢复 Session”的边界。
3. Session 文件上限
非 ephemeral Turn 会调用:
Session.enforce_file_cap
被移出的较早内容可以交给 MemoryStore raw archive。这样 Session 文件不会无限增长,同时保留长期整理入口。
文件上限和模型 replay 上限是两个概念:
文件上限:磁盘 Session 保留多少
replay 上限:本轮送入 ContextBuilder 多少
Runner context budget:每次 Provider 请求真正发送多少4. RESPOND 阶段
_state_respond() 调用 _assemble_outbound(),生成:
OutboundMessage(
channel=msg.channel,
chat_id=msg.chat_id,
content=final_content,
metadata=...
)metadata 可能加入:
_streamedlatency_ms- 原入站 metadata 中需要回传的路由信息
ephemeral direct Turn 还可能加入 _stop_reason。
5. MessageTool 为什么可能抑制默认回复
某些 Agent Turn 会通过 MessageTool 主动向 Channel 发送消息。如果本轮已经这样发送,再返回同一份默认 OutboundMessage 会造成重复。
因此 _assemble_outbound() 会检查 MessageTool 本轮是否已发送,并根据 injection 和 stop reason 决定是否返回 None。
调试“模型完成但没有普通 outbound”时,应先检查:
ctx.suppress_response- MessageTool
_sent_in_turn - 是否已有主动投递
- stop reason
6. RuntimeEventBus 不是 MessageBus
MessageBus 传递用户可见入站/出站消息。
RuntimeEventBus 发布进程内状态事件:
SessionTurnStartedTurnRunStatusChangedTurnCompletedGoalStateChangedRuntimeModelChanged
flowchart LR AL["AgentLoop"] --> OM["OutboundMessage"] OM --> MB["MessageBus"] MB --> CH["Channel / 用户"] AL --> RE["Runtime Event"] RE --> RB["RuntimeEventBus"] RB --> WC["WebuiTurnCoordinator / 订阅者"]
两条路径可能同时发生,但目的不同:
- OutboundMessage:告诉用户“回复是什么”;
- Runtime Event:告诉协调器“Turn 现在处于什么状态”。
7. RuntimeEventPublisher
RuntimeEventPublisher 帮助 AgentLoop:
- 发布 Session turn started;
- 发布 run status;
- 记录当前 Provider/model runtime;
- 记录 latency;
- 发布完成事件;
- 清理本轮临时 runtime 数据。
这避免 AgentLoop 到处手工组装 event。
8. WebUI 协调边界
项目设计约束要求:
- AgentLoop 只发布通用 runtime event;
- WebUI/WebSocket wire 细节交给
WebuiTurnCoordinator或 Channel adapter; _turn_end、_goal_status、标题刷新不进入 AgentLoop 核心。
这条边界对扩展开发非常重要。如果只是增加 UI 状态,不应先改 Runner。
9. 完成时序
简化顺序:
AgentRunner 返回
→ continuation 边界
→ 保存 Session
→ 清 checkpoint
→ 组装 OutboundMessage
→ 发布 outbound
→ 发布 TurnCompleted / run status
→ UI/Channel 各自展示具体发布先后因 direct、MessageBus、stream 和异常分支而有差异,应通过对应入口测试核对。
观察任务
在一次 Tool Call Turn 结束时分别观察:
AgentRunResult.messagesSession.messages- 返回的
OutboundMessage - RuntimeEventBus 订阅者收到的事件
回答:
- 哪些 runner 消息最终保存了?
- tool result 是否被截断或引用?
- latency 保存在哪里?
- Outbound metadata 和 Runtime Event 有哪些不同字段?
源码与测试
nanobot/agent/loop.py_save_turn_state_save_state_respond_assemble_outbound
nanobot/session/manager.pynanobot/bus/events.pynanobot/bus/runtime_events.pynanobot/session/webui_turns.pytests/agent/test_loop_save_turn.pytests/agent/test_runner_persistence.pytests/bus/test_runtime_events.pytests/agent/test_loop_direct_websocket_status.py
自检
- 为什么 Runner messages 不能原样全部写入 Session?
- Session 文件上限和模型 replay 上限有什么不同?
- 为什么 MessageTool 已发送后默认 outbound 可能被抑制?
- Runtime Event 与 OutboundMessage 各自服务谁?
- WebUI wire 细节为什么不应进入 AgentLoop?
本阶段复盘
现在尝试不看文档,画出:
InboundMessage
→ Session
→ TurnState
→ ContextBuilder
→ AgentRunner
→ Provider / Tool
→ AgentRunResult
→ Session save
→ OutboundMessage / Runtime Event如果能解释每条箭头对应的类或函数,就已经掌握 nanobot 核心源码主线。
下一步
进入“状态与长期运行”,深入 Session、Memory、Compaction、Goal、Cron 和 Subagent。