不同Harness的Agent Loop怎么做?Claude Code、Codex等6套方案详解
原创 风儿轻轻er 2026-10-28 20:08 北京

从核心循环到工程细节

一个看似简单的 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
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更多阅读
·

本文转载自微信公众号「PaperWeekly」,查看原文。版权归原作者所有,如有侵权请联系删除。


