Agent Loop:Turn、Step 与协作式取消
用户说一句话,模型可能先读文件、再改配置、最后回一句确认。DeepSeek-Harness(dsh)把这段过程拆成 Turn 与 Step,由默认驱动器 Agent Loop 推进,并把每个动作追加进会话日志。Loop 本身也是插件;定制走瀑布事件,通常不必改主循环源码。
Turn 与 Step
一个 Turn 从用户输入开始,到模型给出本轮最终回复结束:turn/start 开账,turn/end 关账。Turn 里可以有零个或多个 Step。零个也成立,例如请求被取消、尚未形成完整模型回合。
一个 Step 等于一次模型请求,加上这次请求里触发的全部工具调用。边界事件是 step/start / step/end。同一次模型响应可以带多个 tool-call;Loop 按并行安全与独占两类调度。
层级关系固定:Turn 包含 Step,Step 包含「模型请求 + 工具调用」。模型侧历史从会话日志派生,不另存一份对话表;assistant/message、tool/result 等表层事件进派生历史,turn/start、step/start 等仅日志事件模型看不到。
一步里写了什么
典型 Step:先 step/start,再带派生历史与工具 schema 调 LLM;流式结果落成 assistant/message。若有 tool-call,再记 tool/call、执行、tool/result,最后 step/end。下一步请求时,模型已经能看到上一轮工具结果。
1 | session.append('step/start', { turn: 1, step: 1 }) |
无工具时,Step 收束后若没有欠下的工作,直接 turn/end。仍有 tool-call,则再开 Step。所谓 Agent Loop,就是在 Turn 边界内重复「请求 → 工具 → 记日志 → 再请求」,直到模型不再欠工具。
并行、独占与收件箱
同 Step 内多个工具:parallel-safe 可并发,上限默认 maxParallelToolCalls = 10;exclusive 单独跑,形成排序屏障,避免读写同一路径这类互相踩脚。packages/core/agent-loop/src/tool-calls.ts 的 executeToolCalls() 按模型顺序提交结果;取消时给未启动的调用合成错误结果,保证每个 tool/call 都有配对 tool/result,日志可重放。
用户在模型跑着时又发消息,不能硬塞进当前 Step。inbox.ts 的 ReactLoopInbox 拆成 next-turn / next-step 两队,在对应边界 claim。中途插话变成排队,边界处领取。
扩展点与协作式取消
瀑布事件让插件在关键节点改数据或拦截:agent/pre-step 决定是否开 Step,agent/request 可补默认 provider/model,llm/stream 可改流,agent/request-error 可决定是否重试。监听即可定制,不必 fork Loop。
取消是协作式的:触发 AbortSignal 后停掉新工具启动,已启动的跑完(除非工具自己响应 signal),未启动的合成错误结果,已流式交付给用户的文本保留。轮次仍能干净关闭,账本不留下悬空调用。
配置侧可在 profile 里声明式挂 Agent(maxParallelToolCalls、agents[].id/provider/model/cwd 等)。核心实现集中在 packages/core/agent-loop/:agent.ts 管状态机与取消,assistant-stream.ts 管流式帧,runtime-context.ts 投影系统提示与运行时上下文。Agent Loop 是默认发动机,接口在 core/agent;换驱动器或挂插件,都落在同一套 Turn/Step 与日志约定上。
Agent Loop:Turn、Step 与协作式取消