10 分钟 · 读完你能徒手写出 agent loop 的骨架
while(还有工具要执行) { 调模型(流式) → 执行工具 → 结果回填 },
外面再包一层管重试、压缩和排队的 while(true)。全部"智能"来自
stopReason 判定 + 结果回填这两个动作的循环。
Easel 六件套的 ① 号(Harness)借 OpenClaw 实现;OpenClaw 的 loop 本体在
packages/agent-core/src/agent-loop.ts(1562 行),血统来自 badlogic(Mario Zechner)的
pi-mono——被 vendored 进仓库深度改造(THIRD_PARTY_NOTICES 明确致谢)。
gateway 收 /v1/chat/completions
→ agentCommand(准入)
→ runPreparedEmbeddedLoop 外层 while(true):重试/compaction/排队
→ runEmbeddedAttempt 装配工具 + 系统提示词 + 历史
→ runAgentLoop → runLoop agent-loop.ts:214 ← 真正的双层 while
while (hasMoreToolCalls): # 内层:agentic 主循环
① abort 检查(用户喊停就收尾)
② pending/steering 消息提交进 transcript
③ streamAgentResponse 调 LLM # 流式:text_delta 逐个发事件
流中 toolcall_end → 工具提前入队 # 不等流结束就开始干活
④ stopReason 判定:
toolUse → 执行工具 → 继续
error / aborted → 终止
stop → 看 endTurn / terminate 钩子
⑤ tool results 追加回 context.messages # ← 整个"agentic"的秘密就在这行
⑥ turn_end 事件 → prepareNextTurn 钩子 # 可中途换模型/思维档
# hasMoreToolCalls 为假才出内层
排空 followUp 队列;无新消息 → agent_end
⑤ 是第一性设计:工具失败不是抛异常,是构造 isError: true 的结果消息回填——
模型看到失败原因,下一轮自我纠正。用户看到的是 agent"想了一下又试了一次",不是报错。
① compaction(上下文压缩)不是停止。它发生在 assistant 消息之后:
tokens 超过 contextWindow − 16384 预留就触发,LLM 生成摘要,压缩条目
追加进 transcript(历史永不重写,可回放可审计),然后 continue() 续跑。
② 工具是进程内函数,不是子进程。形态统一为
execute(toolCallId, args, signal),MCP 工具也被包装成同形态;串行或并行按批次配置。
③ 同会话严格串行。每会话一个 lane(并发=1)+ Agent.prompt 重入保护;
不同会话不同 lane 天然并行。所以 Easel 才敢让多个浏览器标签页共享一条 raw-stream 文件。
OpenClaw 的流走事件总线;对外部消费者另开一条旁路:
设 OPENCLAW_RAW_STREAM=1 后,所有 text/thinking delta 以 JSONL
best-effort 追加到 /tmp/easel-raw-stream.jsonl(失败静默,绝不影响 run)。
Easel 的 Web 后端每轮开始记住文件尾偏移 → tail 新增行 → 用首个新事件的
runId 闩锁认领自己这轮(隔离并发会话)→ 转 SSE 推给浏览器。
另一处值得抄:Easel 的 supervisor/forward 分离——浏览器断开只断转发,agent 照跑到底,
结果落盘,回来经 /api/chat/last 取回(web/app.py:2064-2066 注释原文)。
长任务 agent 的 Web 集成,这是标准答案。
一手源推荐:
agent-loop.ts(GitHub)
本机也有浅克隆 /tmp/openclaw-src(临时目录)。先只读 runLoop(:214-436)这一段,
对照本课骨架逐行找七件事;再看 docs.openclaw.ai 的
agent-loop 概念文档交叉印证。配套速查:Loop 解剖速查表。