不同Harness的Agent Loop怎么做?ClaudeCode、Codex等6套方案详解
不同Harness的Agent Loop怎么做?ClaudeCode、Codex等6套方案详解
内容摘要
一个看似简单的 Agent Loop,真正做成 Coding Agent 有多复杂?Agent Loop 的基本流程并不复杂。模型发起工具调用,Harness
一个看似简单的 Agent Loop,真正做成 Coding Agent 有多复杂?
一个看似简单的 Agent Loop,真正做成 Coding Agent 有多复杂?
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(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/requestwaterfall:插件决定 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
更多阅读