原文:https://mp.weixin.qq.com/s/GryGhL7SLCPwDQF0VjsPeg
Agent Loop 的基本流程并不复杂。
模型发起工具调用,Harness 执行后将结果返回给模型,模型再决定下一步。围绕这几步,就能搭建一个最简单的 Agent 循环。
但在 Claude Code、Codex 这样的实际产品中,还需要处理大量具体问题:工具如何并行执行、用户能否中途追加指令、权限检查放在哪个环节、上下文接近上限时如何压缩,以及整个循环什么时候结束。
不同 Harness 对这些问题的处理方式并不相同。
Claude Code、Codex、OpenCode、Pi、Oh My Pi 和 DeepSeek Harness 的 Agent Loop,从循环结构、工具执行、上下文管理到扩展机制,逐一梳理了六套系统的具体实现。
六套系统遵循相似的基本流程,但在循环的组织方式、状态管理和扩展机制上,各有不同的设计取舍。
Claude Code
Claude Code 的 agent loop 是一个单线程主循环:调用模型 → 有 tool_use 就执行 → 结果回填 → 再调用,直到模型给出不含工具调用的回复。
循环本身很简单,复杂度都放在工具、权限、hooks、自动压缩和子 agent 上。
循环结构
- 官方把它叫 agentic loop:收集上下文 → 行动 → 验证,三个阶段交织进行。
- 实现上就是一个 while 循环:模型输出里有 tool_use 就继续,没有就结束。
- 一次“模型输出 + 执行工具 + 回填结果”称为一个 turn。
- Agent SDK 暴露的就是同一个循环,CLI / 桌面 / IDE 共用。
一次模型调用
- 每次请求都带上:系统提示、CLAUDE.md、工具定义、完整历史。
- 不变的前缀走 prompt caching,降低重复成本。
- effort 控制推理深度;thinking 块单独输出。
- 每次响应返回后累计 cost,用来做预算检查。
工具执行
- 同一轮多个调用:只读工具(Read / Glob / Grep、标注只读的 MCP)并发执行。
- 改状态的工具(Edit / Write / Bash)串行执行。
- 执行前过权限:PreToolUse hook → deny / allow 规则 → 权限模式 → 询问用户。
- auto 模式用一个分类器模型审查命令和网络请求。
用户插话
- 可以随时中断、换方向、补信息或让它换种做法。
- 流式输入下,运行中发来的消息先排队,并入后续的模型调用。
- 到 maxTurns 时仍在排队的消息不并入,而是另起一个新的 turn。
上下文压缩
- 接近窗口上限自动压缩:先清旧工具输出,再摘要对话。
- 流里发出 compact_boundary;PreCompact hook 可先归档全文。
- 单个超大输出导致反复压缩时,几次后停止并报错,不会死循环。
- 可在 CLAUDE.md 写明“压缩时要保留什么”。
停止条件与子 agent
- 模型回复里没有 tool_use → 结束,返回 ResultMessage(含 cost / usage)。
- maxTurns(只计工具轮)/ maxBudgetUsd 封顶,超了返回错误子类型。
- Stop hook 可以 block 停止,让 Claude 接着干。
- Agent 工具派生子 agent:全新上下文,只把最终回复交回父循环;预算含子 agent。
Agent Loop 运行流程图

核心循环伪代码
*按官方文档还原
messages = [system, CLAUDE_md, *history, user_input] # UserPromptSubmit
while True:
maybe_auto_compact(messages) # PreCompact: 清旧输出 → 摘要
resp = claude.stream(messages, tools, effort) # thinking / text / tool_use
messages.append(resp)
if not resp.tool_uses:
if stop_hook(resp) == "block": # Stop hook 让它继续
messages.append(stop_reason); continue
return Result(resp.text, cost, usage)
allowed = [c for c in resp.tool_uses if permitted(c)] # PreToolUse → 规则 → 模式 → 询问
results = run_parallel([c for c in allowed if c.read_only])
results += run_serial([c for c in allowed if not c.read_only])
messages.append(user(tool_results=results)) # PostToolUse
messages += drain_queued_user_messages() # 运行中排队的插话
if turns >= max_turns or cost >= max_budget:
return Error("error_max_turns / error_max_budget_usd")扩展点:在循环的哪里插手
- UserPromptSubmit:提交输入前;可改写、补上下文或拦截。
- PreToolUse:工具执行前;可拒绝、放行或改参数。
- PermissionRequest:将弹权限框时;可代替用户决定。
- PostToolUse:工具执行后;可给模型追加反馈。
- PostToolBatch:一批工具全部完成后。
- PreCompact / PostCompact:自动或手动压缩前后。
- Stop:将要结束时;可 block 让它继续。
- SubagentStart / SubagentStop:子 agent 起止。
设计取舍
亮点
- 循环本身极简,复杂度都在工具、权限和 hooks 上。
- 读并发、写串行,兼顾速度与安全。
- Stop hook、预算、子 agent 让循环可以被外部编排。
- Hook 事件量丰富,几乎每个阶段都能插手。
代价
- 源码不开源,内部调度细节只能看文档。
- 子 agent 只回传最终回复,父循环看不到中间过程。
- 压缩是有损摘要,早期细节可能丢失。
Claude Code 的循环是“薄循环 + 厚外壳”:循环只管调模型和执行工具,安全与控制都交给权限系统和 hooks。
来源
[1] Claude Code — 工作原理
https://code.claude.com/docs/en/how-claude-code-works
[2] Claude Code — Agent Loop 官方文档
https://code.claude.com/docs/en/agent-sdk/agent-loop
[3] Claude Code — Hooks 参考文档
https://code.claude.com/docs/en/hooks
Codex
Codex(codex-rs)里,一次用户输入启动一个 turn,turn 内循环多次采样:每次采样是一次 Responses API 请求,加上执行这次响应里的全部工具调用。
循环条件是 needs_follow_up:模型还要继续,或者还有用户插话没并入。
循环结构
- 客户端和内核通过 SQ / EQ 通信:提交 Op 进队列,内核推回 EventMsg。
- run_turn() 里是一个 loop:并入插话 → 采样 → 执行工具 → 判断是否继续。
- 一个 Thread 同一时间只有一个活跃 turn。
- turn 开始前先跑 SessionStart / UserPromptSubmit hooks。
一次模型调用
- build_prompt:历史 ResponseItem + 工具定义 + parallel_tool_calls: true。
- 流式接收:文本、推理、计划更新实时推给客户端。
- 断流按 stream_max_retries 自动重试。
- turn_context 固定本轮的模型、沙箱、审批策略等设置。
工具执行
- 边流边执行:OutputItemDone 一到就启动工具 future,不等整段响应结束。
- FuturesOrdered 保证结果按调用顺序回填。
- 并行闸门是读写锁:支持并行的工具拿读锁并发,其它拿写锁独占。
- ToolOrchestrator:审批 → 选沙箱 → 执行 → 被沙箱拦截则升级重试(不重复审批)。
用户插话
- turn 运行中可以 turn/steer:消息进 input_queue,不断开 turn。
- 每次采样前取出 pending input 并入(先过 UserPromptSubmit hook)。
- 还有没并入的输入也算 needs_follow_up,循环会继续。
- turn/interrupt 中断当前 turn,并记录 turn_aborted。
上下文压缩
- 采样前检查:超过 model_auto_compact_token_limit 先压缩。
- 采样后如果还要继续且已到上限,原地压缩后接着跑(也支持远端压缩)。
- 换成窗口更小的模型时,会先用旧模型压一遍。
- 压缩结果写成 Compacted 记录,替换历史。
停止条件与多 agent
- needs_follow_up 为假 → 运行 Stop hooks。
- Stop hook 可以 block,并把续写提示作为新输入注入,循环继续。
- 否则推送 TurnComplete 到 EQ,turn 结束。
- 多 agent:子 agent 是独立 Thread,父 turn 通过工具收结果。
Agent Loop 运行流程图

核心循环伪代码
*简化自 codex-rs/core/src/session/turn.rs
async fn run_turn(sess, input) {
run_hooks_and_record_inputs(&input).await; // SessionStart / UserPromptSubmit
run_pre_sampling_compact().await; // 超阈值先压缩
loop {
let pending = input_queue.get_pending_input(); // turn/steer 插话
run_hooks_and_record_inputs(&pending).await;
let r = run_sampling_request(build_prompt(history, tools)).await;
// 流式事件里 OutputItemDone(FunctionCall) → in_flight.push_back(tool_future)
// 工具:RwLock 读并行 / 写独占;审批 → 沙箱 → 执行 → 升级重试
drain_in_flight().await; // 按顺序回填 FunctionCallOutput
let follow = r.needs_follow_up || input_queue.has_pending_input();
if follow && token_limit_reached { run_auto_compact().await; continue; }
if !follow {
match run_turn_stop_hooks().await {
Block(prompt) => record(prompt), // Stop hook 要求继续
_ => break,
}
}
}
send_event(TurnComplete); // 推到 EQ
}
扩展点:在循环的哪里插手
- SessionStart:会话开始 / 恢复 / 压缩后。
- UserPromptSubmit:每条输入(含 steer)并入前。
- PreToolUse:工具执行前;可拦截。
- PermissionRequest:需要审批时。
- PostToolUse:工具执行后。
- Stop:turn 将结束;可 block 并给续写提示。
设计取舍
亮点
- 工具边流边跑,长响应里的调用不必等到最后。
- 沙箱 + 审批 + 升级重试统一在编排器里,安全模型最完整。
- steer 并入当前 turn,插话不会打断节奏。
- 压缩分采样前、采样中两个时机,长任务不容易爆窗口。
代价
- 一个 Thread 同时只有一个活跃 turn。
- 工具能否并行靠工具自报,默认保守(独占)。
- 逻辑分散在 turn.rs / tasks / tools 多个模块,读起来较重。
Codex 的循环围绕“采样”组织:边流边执行工具,安全由沙箱编排器兜底,needs_follow_up 决定是否再采样。
来源
[1] OpenAI — Codex 官方仓库
https://github.com/openai/codex
[2] Codex — Agent Loop 主循环(turn.rs)
https://github.com/openai/codex/blob/main/codex-rs/core/src/session/turn.rs
[3] Codex — 工具并行执行(parallel.rs)
https://github.com/openai/codex/blob/main/codex-rs/core/src/tools/parallel.rs
[4] Codex — 工具执行编排(orchestrator.rs)
https://github.com/openai/codex/blob/main/codex-rs/core/src/tools/orchestrator.rs
[5] Codex — Hooks 运行时(hook_runtime.rs)
https://github.com/openai/codex/blob/main/codex-rs/core/src/hook_runtime.rs
[6] Codex — Hooks 模块
https://github.com/openai/codex/blob/main/codex-rs/hooks/src/lib.rs
OpenCode
OpenCode 的循环由 SessionRunner 驱动:从 SQLite 里的 inbox 取输入,按 step 循环;每个 step 是一次模型请求,有工具调用就再来一步。
工具在流里一出现就执行,每个 step 前后各抓一次 Git 快照,压缩、重试、续写都在 step 内部处理。(基于当前 dev 分支的 core 重写版)
循环结构
SessionRunner.drain:取一条输入,然后 while 按 step 推进。- 每个 step 一次模型请求;
needsContinuation(有工具调用)就 step++ 再来。 - runStep 内部还有一层循环,专门处理压缩、重试和续写。
- 用 Effect 编写,状态变化通过事件总线推给各个客户端。
一次模型调用
- 每个 step 重新加载上下文:instructions、agent、模型、工具、历史。
- 超过
agent.steps时追加 MAX_STEPS 提示,并设置toolChoice: none。 - 请求发出的同时并行抓 step-start 快照,不拖慢首字。
- 最后一步也保留工具定义,以命中 provider 的前缀缓存。
工具执行
- 流里一出现 tool-call 事件就执行(fiber 并发),不等整段响应结束。
- provider 自己执行的工具(providerExecuted)跳过本地执行。
- 权限规则 allow / ask / deny;ask 会弹窗,拒绝时抛 DeclinedError。
- 参数没写完的调用不执行,回给模型“请重新发起”。
用户插话(Inbox)
- inbox 每条都带
delivery:queue(等空闲)或 steer(运行中插入)。 - 运行中只取 steer;到了空闲边界才取一条 queue 输入。
- inbox 还能装控制项:compaction(手动压缩)、move(迁移会话位置)。
- 进程重启后,先清理残留的压缩和工具调用,再继续。
压缩与失败恢复
- 每个 step 前跑 compaction.compact(auto),压完重新加载再继续。
- 请求报上下文溢出 → 以 overflow 为原因压缩一次后重来。
- 可重试错误按策略等待;断流时合成一句“请从中断处继续”。
- attempt 结果:Completed / Retry / Continue / Compacted / RecoverFull。
停止条件与子 agent
- needsContinuation 为假(没有工具调用)→ 本次 drain 完成。
- step 结算:结束快照 → patch 文件差异 → tokens / cost。
- 达到 agent.steps 上限时,强制只输出文字总结。
- 子 agent 以子 Session 运行,完成后把结果回传父会话。
Agent Loop 运行流程图

核心循环伪代码
*简化自 packages/core/src/session/runner/llm.ts、step.ts
drain(sessionID):
settleStaleCompactions(); settleStaleToolCalls() # 崩溃后的收尾
whiletrue:
next = advanceToStep() # 从 inbox 提升 queue / steer 输入
if next != Ready: return next # Complete / Moved / Reloaded
continuing = runStep(next.context, step); step += 1
runStep(ctx, step):
whiletrue: # 压缩 / 重试 / 续写循环
if compaction.compact(reason="auto").completed: continue
req = buildRequest(ctx, maxStepsPrompt if step >= agent.steps)
outcome = attempt(req) # 流里 tool-call → 立即执行(并发)
match outcome: # 权限 allow / ask / deny
Completed(needsContinuation) -> return needsContinuation
Retry -> retry.wait()
Continue -> retry.wait(); synthetic("请从中断处继续")
Compacted -> 上下文溢出,已压缩,重来
扩展点:在循环的哪里插手
- tool · execute.before:工具执行前;可拒绝调用。
- tool · execute.after:工具执行后;可改写输出。
- permission · evaluate:权限判定时参与决策。
- session · request 等:请求构造、压缩、标题生成等环节。
- aisdk:改 AI SDK / provider 参数。
- 事件总线(SSE):所有状态变化实时推给 TUI / 桌面 / IDE。
设计取舍
亮点
- 工具边流边跑,step 状态全在数据库,崩溃后可恢复。
- 每个 step 前后各一次 Git 快照,可精确回退到 step。
- inbox 统一承载输入和控制项(压缩、迁移)。
- step 上限到了强制总结,避免无限循环。
代价
- 依赖 SQLite + 事件总线,比纯日志实现更重。
- Effect 风格代码门槛较高。
- core 正在重写(dev 分支),实现变化快。
OpenCode 把 agent loop 做成“数据库驱动的 step 机”:输入、状态、快照都落库,客户端只订阅事件。
来源
[1] opencode — 官方仓库
https://github.com/anomalyco/opencode
[2] opencode — SessionRunner 入口(index.ts)
https://github.com/anomalyco/opencode/blob/dev/packages/core/src/session/runner/index.ts
[3] opencode — 模型调用与循环处理(llm.ts)
https://github.com/anomalyco/opencode/blob/dev/packages/core/src/session/runner/llm.ts
[4] opencode — 输入队列(inbox.ts)
https://github.com/anomalyco/opencode/blob/dev/packages/core/src/session/inbox.ts
[5] opencode — 权限控制(permission.ts)
https://github.com/anomalyco/opencode/blob/dev/packages/core/src/permission.ts
[6] opencode — 插件包定义
https://github.com/anomalyco/opencode/blob/dev/packages/plugin/package.json
Pi
Pi 的 agent loop 是一个极简双层循环:内层在“还有工具调用或 steering 插话”时继续调模型,外层在要停时检查 follow-up。
循环只认几个回调(prepareNextTurn / prepareRequest / beforeToolCall / afterToolCall / finishTurn),压缩、重试、扩展全由外面的 AgentSession 挂上去。
循环结构
- agentLoop() → runLoop:内层 while 处理工具和 steering,外层 while 处理 follow-up。
- 内层条件:hasMoreToolCalls || pendingMessages.length > 0。
- 全程用 AgentMessage,只在调模型那一刻 convertToLlm。
- 对外是事件流:agent_start / turn / message / tool_execution_* / agent_end。
一次模型调用
- prepareNextTurn:上一轮结束后可换上下文、模型、思考级别。
- 注入消息前先算工具增删,写成 system 消息(toolsAdded / toolsRemoved)。
- prepareRequest:AgentSession 从会话树投影上下文,虚拟模型在这里路由。
- transformContext → convertToLlm → 流式调用。
工具执行
- 默认 parallel:先逐个 prepare(校验 + beforeToolCall),再 Promise.all 并发执行。
- 批次里只要有一个 executionMode: sequential 的工具,整批改为串行。
- afterToolCall 可改写结果;结果按模型给出的顺序回填。
- 所有结果都带 terminate 时,提前结束内层循环。
用户插话
- steering:运行中插话,在每一轮之间注入(包括压缩等长准备之后再取一次)。
- follow-up:agent 本来要停时才取,让外层再跑一圈。
- 两个队列都可选 one-at-a-time(默认)或 all。
- finishTurn 可返回 continue,不加新消息也再跑一轮。
压缩与失败恢复
- stopReason 为 error / aborted:直接结束本次 run,不在循环里重试。
- AgentSession 在 run 结束后:可重试错误 → 退避自动重试(maxRetries)。
- 上下文溢出 → 压缩后重试;超阈值 → 压缩但不重试;轮间准备阶段也会检查阈值。
- 输出被 length 截断:整批工具调用不执行,回错误让模型重发。
停止条件与子 agent
- 没有工具调用、也没有 steering → 内层结束。
- 没有 follow-up、也没有 continue 决定 → agent_end。
- 扩展的 finishTurn 返回 end 可以随时提前结束。
- 核心刻意不带子 agent 和 plan mode,需要就用扩展或 package 自己加。
Agent Loop 运行流程图

核心循环伪代码
*简化自 packages/agent/src/agent-loop.ts
let pending = await getSteeringMessages()
while (true) { // 外层:follow-up
let hasMoreToolCalls = true
while (hasMoreToolCalls || pending.length > 0) { // 内层:工具 + steering
if (lastTurn) await prepareNextTurn(lastTurn) // 换模型 / 上下文、可压缩
inject(declareToolChanges(ctx, pending)); pending = []
await prepareRequest(ctx) // 会话投影 · 模型路由
const msg = await streamAssistantResponse(ctx) // transformContext → convertToLlm
if (msg.stopReason === 'error' || msg.stopReason === 'aborted') return end()
const batch = msg.stopReason === 'length'
? failTruncatedToolCalls(msg) // 截断可能截断,不执行
: await executeToolCalls(msg) // parallel / sequential
hasMoreToolCalls = msg.toolCalls.length > 0 && !batch.terminate
const d = await finishTurn(lastTurn = {msg, batch}) // end / continue
if (d?.action === 'end') return end()
pending = await getSteeringMessages()
}
const followUp = await getFollowUpMessages()
if (followUp.length) { pending = followUp; continue }
if (explicitContinuation) continue // finishTurn 要求再跑一轮
break
}
emit('agent_end') // AgentSession: 重试 / 压缩扩展点:在循环的哪里插手
- prepareNextTurn:每轮之间;换上下文、模型、思考级别。
- prepareRequest:请求前;投影上下文、路由模型。
- transformContext / convertToLlm:改写上下文、转成模型消息。
- beforeToolCall → tool_call:工具前;扩展可拦截、改参。
- afterToolCall → tool_result:工具后;扩展可改写结果。
- finishTurn → turn_end:每轮结束;扩展可 end / continue。
- before_agent_start:run 开始前;可改系统提示、工具。
- agent_before_settle:run 将停时;扩展可让它继续。
设计取舍
亮点
- 循环不到一千行,双层结构一眼能看懂。
- 所有策略都是回调,压缩、重试、路由都能替换。
- 工具声明 sequential 就自动降级串行,并发安全由工具自报。
- 截断的工具调用一律不执行,避免拿残缺参数做事。
代价
- 重试和溢出恢复都在 run 结束后,失败要重跑整轮请求。
- 没有内置权限审批、安全靠扩展自己写。
- 不带子 agent、plan mode,复杂编排要自己搭。
Pi 把 agent loop 压到最小:循环只管“调模型 → 跑工具 → 收插话”,其余能力都通过回调和扩展挂上去。
来源
[1] Pi — 官方仓库
https://github.com/earendil-works/pi
[2] Pi — Agent Loop 核心实现(agent-loop.ts)
https://github.com/earendil-works/pi/blob/main/packages/agent/src/agent-loop.ts
[3] Pi — Agent 状态与调用(agent.ts)
https://github.com/earendil-works/pi/blob/main/packages/agent/src/agent.ts
[4] Pi — 会话管理(agent-session.ts)
https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/agent-session.ts
[5] Pi — 项目说明
https://github.com/earendil-works/pi/blob/main/README.md
Oh My Pi
Oh My Pi 继承 Pi 的双层循环:外层处理 follow-up,内层在“还有工具调用或插话”时持续调用模型。
它在上面加了工具并发分级、可中断工具、TTSR 流内规则、LoopGuards 纠偏和 advisor 旁路审查,重试与压缩由外面的 AgentSession 负责。
循环结构
- agentLoop() → runLoop:外层 while 处理 follow-up,内层 while 处理工具和 steering。
- 内层条件:hasMoreToolCalls || pendingMessages.length > 0。
- 对外是事件流:agent_start / turn_start / message / tool_execution / turn_end / agent_end。
- AgentSession 包在外面,负责持久化、重试、压缩、advisor 等。
一次模型调用
- transformContext(扩展可改写)→ convertToLlm → 规范化成 provider 消息。
- 支持的 provider 可以在流中途直接接收 live steering。
- TTSR:流里命中规则就中止本次输出,注入规则后重来。
- 设了 deadline 时超时即停,不再发下一次请求。
工具执行
- 先 prepare:参数校验 + beforeToolCall 钩子(可改参 / 拒绝)。
- concurrency:shared 并发执行,exclusive 等前面全部完成后独占。
- afterToolCall 钩子处理结果;结果按批次顺序发出。
- 找不到工具时,给模型提示相近的工具名。
用户插话
- steering:运行中插话,在工具批次边界注入。
- 可中断工具(wait 等)会被 steering 立刻打断;bash 等只收软信号,不会被杀。
- asides:不打断的旁注(如 advisor 便签),随下一轮带上。
- follow-up:等 agent 完全停下后再开新一轮。
失败恢复与纠偏
- stopReason 为 error / aborted:为未完成的 toolCall 补占位结果。
- turn-recovery:瞬时错误重试,额度受限时等待或切到备用模型链。
- 自动压缩、自动重试在 AgentSession 层,turn 结束后检查。
- LoopGuards:同一工具调用反复出现时,注入纠偏提示。
停止条件与子 agent
- 没有 toolCall、也没有 steering → 内层结束。
- 再查迟到的 steering、asides、follow-up,有就让外层再跑一圈。
- 都没有 → agent_end。
- task 工具派生子 agent,可隔离运行。
Agent Loop 运行流程图

核心循环伪代码
*简化自 packages/agent/src/agent-loop.ts
let pending = await getSteeringMessages()
while (true) { // 外层:follow-up
while (hasMoreToolCalls || pending.length > 0) { // 内层:工具 + steering
inject(pending); pending = []
const ctx = convertToLlm(await transformContext(messages))
const msg = await streamAssistantResponse(ctx) // TTSR 命中 → 中止 + 注入规则 + 重来
if (msg.stopReason === 'error' || msg.stopReason === 'aborted') {
emitPlaceholderToolResults(msg); return // AgentSession 负责重试
}
const prepared = await prepareToolCallDispatch(msg) // 校验 + beforeToolCall
const results = await executeToolCalls(prepared) // shared 并发 / exclusive 屏障
emit('turn_end') // 外层:LoopGuards、压缩、advisor
hasMoreToolCalls = msg.toolCalls.length > 0
pending = [...await getSteeringMessages(), ...asides()]
}
pending = [...lateSteering, ...asides(), ...await getFollowUpMessages()]
if (pending.length === 0) break
}
emit('agent_end')扩展点:在循环的哪里插手
- transformContext:每次请求前改写上下文。
- convertToLlm:把 AgentMessage 转成模型消息。
- beforeToolCall:工具前;可改参、拒绝、附加上下文。
- afterToolCall:工具后;可改写结果。
- getSteering / FollowUp / Aside:三种插话队列的取数回调。
- extensions / hooks runner:扩展可订阅全部 agent 事件。
设计取舍
亮点
- 沿用 Pi 的双层循环,结构简单好懂。
- 插话分软硬:等待类工具立刻打断,有副作用的工具不被硬杀。
- TTSR、LoopGuards、advisor 把“纠偏”做进了循环。
- shared / exclusive 让工具自己声明并发安全性。
代价
- agent-loop.ts 约 4000 行,边界处理很多。
- 很多能力在 AgentSession 层(约 1.3 万行),循环和会话耦合。
- TTSR 中止重来会多花 token。
Oh My Pi 保留了 Pi“外层 follow-up + 内层工具”的简单骨架,再在流中、工具批次边界和 turn 结束三处加入纠偏能力。
来源
[1] oh my pi — 官方仓库
https://github.com/can1357/oh-my-pi
[2] oh my pi — Agent Loop 核心实现(agent-loop.ts)
https://github.com/can1357/oh-my-pi/blob/main/packages/agent/src/agent-loop.ts
[3] oh my pi — AgentSession 实现(agent-session.ts)
https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/session/agent-session.ts
[4] oh my pi — 流式调用与会话机制说明
https://github.com/can1357/oh-my-pi/blob/main/docs/provider-streaming-internals.md
DeepSeek Harness
DeepSeek Harness(dsh)的 ReactLoopAgent 是一个可观察的状态机:turn → step → attempt 三层,每个边界都写进 Session 日志。
输入走两条持久队列(next-turn / next-step),压缩、目标续跑、审批都是挂在少数扩展点上的插件。
循环结构
- driver:kick() 里 while (await turn()),有积压输入就连续开 turn。
- 三层:turn(一次任务)→ step(一次请求 + 本次工具)→ attempt(失败重试)。
- 每个边界都落日志:turn/start、step/start、…、step/end、turn/end。
- 每次请求都从日志投影(Surface)构造,不在内存另存历史。
一次模型调用
- agent/request waterfall:插件决定 provider / model / effort。
- 系统提示、运行时上下文按需写成 system/message、user/message。
- 流式帧实时广播(agent/assistant-stream),结束落 assistant/message。
- 失败落 assistant/attempt,由 agent/request-error 决定是否重试。
工具执行
- executeToolCalls:独占调用是屏障,并行调用进有界滚动池。
- 执行可以交叠,但结果和附加上下文按模型顺序提交。
- 调度分 prepare → dispatch → finalize,审批与钩子都落成事件。
- 中止时未启动的调用补合成错误结果,保证回放合法。
用户插话(Inbox)
- 两条持久队列:next-turn 与 next-step。
- followup → next-turn 并唤醒;steer → next-step 并唤醒;inject → next-step 不唤醒。
- 每个 step 前 preStep 领取 next-step(新 turn 再领一条 next-turn)。
- 取消默认清空队列;keepInbox 可保留。
压缩与失败恢复
- 压缩是插件:挂在 agent/pre-step,超阈值先压缩。
- 请求报上下文溢出时,挂在 agent/request-error 压缩后重试。
- 中断时保存已输出前缀(interrupted: true);失败尝试不进模型可见历史。
- step 失败时为挂起的工具调用补 tool/result 再关闭 step。
停止条件与外层编排
- 无 tool-call → completed;输出到上限 → max-tokens(粘性,不会被后续覆盖)。
- 工具结果带 concludesTurn 也结束 turn;pre-step 拒绝 → blocked。
- 将停且 next-step 为空时触发 agent/turn-stopping,插件可再塞输入。
- 外层编排是插件:Goal(同会话续跑)与 Ralph(每轮新开子 agent)。
Agent Loop 运行流程图

核心循环伪代码
*简化自 packages/core/agent-loop/src/agent.ts
while (await this.turn()) {} // driver: inbox 有货就开下一个 turn
async turn() {
log('turn/start')
while (true) {
const d = await preStep(target) // 领 inbox + agent/pre-step(压缩插件在这)
if (d.kind === 'reject') { ends = 'blocked'; break }
log('step/start')
for (;;) { // attempt 循环
const cfg = await waterfall('agent/request') // 选 provider / model / effort
msg = await stream(buildRequest(surface)) // 从日志投影出请求
if (!failed) { log('assistant/message'); break }
log('assistant/attempt')
if ((await waterfall('agent/request-error'))?.kind !== 'retry') throw err
}
if (msg.toolCalls.length) await executeToolCalls() // 屏障 + 并行池、按序提交
else ends = 'completed'
log('step/end')
if (ends && inbox.nextStep.length === 0) await serial('agent/turn-stopping')
if (ends && inbox.nextStep.length === 0) break
target = 'next-step'
}
log('turn/end', { reason: ends })
return inbox.hasPending
}扩展点:在循环的哪里插手
- agent/pre-step:waterfall:每个 step 前 enter / reject,可改写输入。
- agent/request:waterfall:决定本次请求的模型配置。
- agent/request-error:waterfall:请求失败时决定是否重试。
- agent/turn-stopping:serial:turn 将停时,可塞入新输入。
- agent/inbox/*:inserted / claimed / discarded。
- agent/status:idle ↔ running 状态切换。
设计取舍
亮点
- 每个边界都是日志事件,崩溃、回放、fork 都能对齐到 step。
- 状态机只开放少数 waterfall 扩展点,压缩、目标、审批全是插件。
- 工具并行执行、按序提交,速度和可重放兼得。
- turn 的结束原因结构化记录,便于 UI 和统计。
代价
- 概念多(turn / step / attempt / Surface / inbox),上手成本高。
- 行为取决于插件的 waterfall 顺序,排查要看日志。
- 项目较新,接口仍在快速演进。
DeepSeek Harness 把 agent loop 做成了“写日志的状态机”:循环只推进 turn / step,所有策略都通过扩展点插件化。
来源
[1] DeepSeek Harness — 官方仓库
https://github.com/deepseek-ai/deepseek-harness
[2] DeepSeek Harness — Agent Loop 核心实现(agent.ts)
https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/agent-loop/src/agent.ts
[3] DeepSeek Harness — Agent Loop 模块说明
https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/agent-loop/README.md
[4] DeepSeek Harness — 上下文压缩模块
https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/compaction/README.md
[5] 不同harness的agent loop是怎么做的
https://xhslink.cn/o/8vbCd3WFQjy