claw-code 源码详细分析:Turn Loop 里的工程细节——多轮对话如何在移植期保持可测试、可回放?
路径result/04.md若你习惯写reuslt对应到仓库的result/目录即可。涉及源码src/runtime.pyrun_turn_loop、bootstrap_session、src/query_engine.py、src/session_store.py、src/transcript.py、src/history.py、src/main.py、tests/test_porting_workspace.py。1. Turn Loop 在仓库里的定位PortRuntime.run_turn_loop不是「模拟用户与模型你来我往的真实多轮产品逻辑」而是一个移植期探针在同一组路由结果上对同一个QueryEnginePort实例连续调用多次submit_message观察会话状态mutable_messages、total_usage、transcript_store如何累积stop_reason何时从completed变为max_turns_reached或max_budget_reached--structured-output打开后每一轮TurnResult.output是否仍是稳定可解析的 JSON。这样做的直接好处是无网络、无 API Key、无随机温度多轮行为在 CI 里可重复满足「移植期可测试」可回放则通过另一条链路——持久化 JSON load_session/from_saved_sessionreplay_user_messages——部分实现下文分述。2.run_turn_loop循环与引擎配置的耦合def run_turn_loop(self, prompt: str, limit: int 5, max_turns: int 3, structured_output: bool False) - list[TurnResult]: engine QueryEnginePort.from_workspace() engine.config QueryEngineConfig(max_turnsmax_turns, structured_outputstructured_output) matches self.route_prompt(prompt, limitlimit) command_names tuple(match.name for match in matches if match.kind command) tool_names tuple(match.name for match in matches if match.kind tool) results: list[TurnResult] [] for turn in range(max_turns): turn_prompt prompt if turn 0 else f{prompt} [turn {turn 1}] result engine.submit_message(turn_prompt, command_names, tool_names, ()) results.append(result) if result.stop_reason ! completed: break return results2.1 工程细节一外层循环次数与QueryEngineConfig.max_turns同名同值CLI 把--max-turns同时传给for turn in range(max_turns)——最多尝试几轮submit_messageQueryEngineConfig(max_turnsmax_turns)——引擎内部允许写入mutable_messages的最大条数见query_engine里len(self.mutable_messages) self.config.max_turns的闸门。在默认用法下两者一致因此常见情况是每一轮都能completed直到跑满循环次数。若将来有人把「循环次数」与「引擎 max_turns」拆成不同参数第N轮可能提前拿到stop_reasonmax_turns_reachedrun_turn_loop会因result.stop_reason ! completed提前break——这是可测试的显式出口而不是死循环或静默失败。2.2 工程细节二路由只做一次多轮共用command_names/tool_namesroute_prompt(prompt, limit)在循环外调用一次整段 loop 复用同一matches推导出的command_names/tool_names。含义是移植期专注测QueryEngine 状态机 输出格式不引入「每轮重新理解意图」的变量。产品期局限真实多轮里用户意图会变路由应每轮更新当前实现是刻意简化阅读时勿误认为已是完整对话产品。2.3 工程细节三合成 turn 文本保证每轮输入可区分turn_promptpromptifturn0elsef{prompt}[turn{turn1}]同一语义骨架下微调字符串使得mutable_messages中各条不相等便于 diff 与调试UsageSummary.add_turn按词数累加时每轮输入长度略有变化预算边界可通过调整max_budget_tokens在测试中触发。2.4 工程细节四不传denied_tools空元组循环里固定denied_tools()多轮不累积权限拒绝。与bootstrap_session带_infer_permission_denials对比Turn Loop 走的是「纯会话闸门 用量」切片测试权限审计在单轮 bootstrap 报告里练。3. 可测试性TurnResult列表即「黄金轨迹」run_turn_loop返回list[TurnResult]每一元素包含本轮输入、输出、匹配元数据、用量快照、停止原因dataclass(frozenTrue) class TurnResult: prompt: str output: str matched_commands: tuple[str, ...] matched_tools: tuple[str, ...] permission_denials: tuple[PermissionDenial, ...] usage: UsageSummary stop_reason: str单测/CI 可以断言轮次数例如test_turn_loop_cli_runs使用--max-turns 2检查输出中出现## Turn 1与stop_reason见tests/test_porting_workspace.py。某一固定prompt 固定快照下matched_tools是否非空test_bootstrap_session_tracks_turn_state针对 bootstrap同一套路可迁到纯QueryEnginePort单测。structured_outputTrue时每轮output是否为合法 JSON当前实现为json.dumps包一层summarysession_id。冻结数据面命令/工具来自reference_data/*.json路由算法确定性强无模型随机性这是「移植期可测试」的基石。4. 可回放三条互补路径4.1 内存轨迹results: list[TurnResult]调用方若拿到run_turn_loop的返回值已具备按轮重放「当时引擎认为发生了什么」的只读记录适合单元测试内联断言。4.2 转写replayTranscriptStore.replay()/QueryEnginePort.replay_user_messagesdef replay(self) - tuple[str, ...]: return tuple(self.entries)def replay_user_messages(self) - tuple[str, ...]: return self.transcript_store.replay()submit_message在成功路径上会对mutable_messages与transcript_store同步append因此用户侧消息序列可通过replay_user_messages()取出用于不依赖磁盘的轻量回放或与其他模块对拍。注意run_turn_loop未调用persist_session因此默认 Turn Loop 结束后若不做持久化磁盘上没有该次多轮的 JSON回放仅限进程内或通过自写测试保存TurnResult。4.3 磁盘会话persist_sessionload_sessionfrom_saved_sessionbootstrap_session在单轮结束后调用engine.persist_session()把session_id、messages、累计 input/output 伪 token写入.port_sessions/id.jsondef persist_session(self) - str: self.flush_transcript() path save_session( StoredSession( session_idself.session_id, messagestuple(self.mutable_messages), input_tokensself.total_usage.input_tokens, output_tokensself.total_usage.output_tokens, ) ) return str(path)测试链路test_load_session_cli_runsbootstrap → 取persisted_session_path的 stem →load-session验证持久化与读取闭环。classmethod def from_saved_session(cls, session_id: str) - QueryEnginePort: stored load_session(session_id) transcript TranscriptStore(entrieslist(stored.messages), flushedTrue) return cls( manifestbuild_port_manifest(), session_idstored.session_id, mutable_messageslist(stored.messages), total_usageUsageSummary(stored.input_tokens, stored.output_tokens), transcript_storetranscript, )可回放含义新进程可from_saved_sessionhydrate引擎继续submit_message若业务允许实现跨运行的会话延续。当前StoredSession不保存每轮TurnResult.output与累积permission_denials因此严格说是「用户消息 用量摘要」级回放不是完整对话录移植期够用产品期需扩展 schema。4.4 人类可读报告RuntimeSession.as_markdownHistoryLog单轮bootstrap_session把上下文、setup、路由、执行 shim、流式事件、turn_result、持久化路径与HistoryLog事件链打成一篇 Markdownhistory.add(routing, fmatches{len(matches)} for prompt{prompt!r}) history.add(execution, fcommand_execs{len(command_execs)} tool_execs{len(tool_execs)}) history.add(turn, fcommands{len(turn_result.matched_commands)} tools{len(turn_result.matched_tools)} denials{len(turn_result.permission_denials)} stop{turn_result.stop_reason}) history.add(session_store, persisted_session_path)def as_markdown(self) - str: lines [# Session History, ] lines.extend(f- {event.title}: {event.detail} for event in self.events) return \n.join(lines)适合PR 审查、故障单附件一眼看到路由与 stop 原因无需复现者本地再跑模型。5. 与bootstrap_session的对比单轮 vs 多轮维度bootstrap_sessionrun_turn_loopsubmit_message次数1最多max_turns权限拒绝_infer_permission_denials(matches)固定()流式事件stream_submit_message→stream_events不收集持久化persist_session无HistoryLog有无典型用途单轮「全链路报告」 落盘 session id压状态机 / 预算 / 结构化输出二者互补bootstrap 练「审计叙事与落盘」turn-loop 练「多轮状态累积与早停」。6. CLI 与测试如何锁住行为CLIsrc/main.pyif args.command turn-loop: results PortRuntime().run_turn_loop(args.prompt, limitargs.limit, max_turnsargs.max_turns, structured_outputargs.structured_output) for idx, result in enumerate(results, start1): print(f## Turn {idx}) print(result.output) print(fstop_reason{result.stop_reason}) return 0测试test_turn_loop_cli_runs子进程跑turn-loop断言输出含## Turn 1与stop_reason——端到端、无 mock与test_bootstrap_cli_runs、test_load_session_cli_runs一起构成会话相关的回归网。7. 移植期建议与演进方向基于现状可测试保持TurnResult稳定路由与快照 JSON 固定Turn Loop 继续作为确定性压力小工具。可回放若要多轮落盘可在run_turn_loop末尾可选调用persist_session或扩展StoredSession保存每轮output/stop_reason。真实多轮将route_prompt移入循环内并传入上一轮 assistant 输出或工具结果才接近产品语义当前实现是有意减变量的脚手架。预算测试调低QueryEngineConfig.max_budget_tokens需在run_turn_loop暴露参数或构造专用测试入口可稳定触发max_budget_reached与提前break分支。8. 小结Turn Loop通过确定性路由 冻结的 matched 元组 合成 turn 文本在无 LLM条件下演练多轮状态累积与停止语义并以list[TurnResult]作为可断言轨迹。可回放依赖TranscriptStore/replay_user_messages进程内与persist_sessionload_session/from_saved_session跨进程Turn Loop 默认不落盘与 bootstrap 分工明确。HistoryLog Markdown 报告提供人类可读的单轮审计切片与多轮机器可断言结果形成测试金字塔的两层。