跳转至

07 · AgentLoop 全路径:创建、收信、请求、重试、工具与结束

本章按完整函数上下文走默认 loop。关键 agent.ts、inbox、runtime-context、tool-calls 在锁定 main 与 rc.2 的行为一致;main 移除 companion invariant 插件,不能把“有独立诊断插件”当成当前核心 API。本章步骤号是讲解编号,不是源码内增加的新阶段。

7.1 AgentRegistry 与具体 AgentLoop 的职责

AgentRegistry 维护 live Agent、公用 factory、父子关系和 AsyncLocalStorage initiator;具体 loop 负责创建/恢复机器与 Session。工具调度用 requireInitiator 知道当前工作是谁发起的;宿主驱动 goal/schedule 用 withoutInitiator,避免自动动作误被归属成一个旧 tool 调用。公共分离。

自己的应用应使用 registry 的 create({sessionId,agentOptions,setup,…}) 返回 handle。Session 与 Agent 使用同一个 SessionId;setup 在未发布阶段完成 scope tools/prompt/listeners,await 后可返回同步 commit 做最终重验;setup “只组合,不驱动”是 trusted same-process 的契约,收到 Agent 对象不表示可以提前 prompt。create resolve 后再 followup/steer,finally handle.dispose。创建选项。

7.2 工厂为什么有这么多拥有者

FactoryOwnership 收集 live agent disposers 和 startupTasks;停止接受新工作,abort factory signal,然后 await live teardown 和 startup continuation。INACTIVE_STATES 是 UNLOADING/DISPOSED/FAILED。配置 maxTokens 要 positive safe integer,configured agents 的 sessionId 与 resumeSessionId 互斥,exact id 不能重复。

配置中 stable sessionId 可在 remount 时 restoreOrCreate;只有真正的 SessionPersistenceNotFoundError 才 fallback create,腐坏、冲突和存储失败都不 fallback。launcher identities 覆盖 config 身份键,使 model overlay 不会把外部选定 Session id 丢掉。工厂所有权,配置验证。

prepare 先注册 caller/factory abort relay和 memoized dispose,才创建 ReactLoopAgent/scope。publish 先 enter Session、enter Agent,再 announce Session、await agent/created serial 初始化;publication barrier 保证 dispose 不在创建监听器 await 途中破坏它仍使用的资源。initializeAgent 使用 maintenance 阻止 startup 期间队列立刻运行,失败 rollback scope/registry/handle。创建事务。

teardown 不是 fire-and-forget:等待 publication,cancel(disposed),whenIdle,scope.dispose,write handle.close,再 detach agent/session,最后取消 owner bookkeeping;失败收集到一个或多个错误,不因第一个失败丢掉余下清理。scope 的早释放和 handle 的晚关闭是代码实际顺序,不应只画一个笼统“清理所有东西”。

7.3 Phase 与对外 status

机器 phase 有 idle、maintenance、running。maintenance 对外 status 仍为 idle,但已有 activityDone,runMaintenance 不允许与其他 active work 重叠。running 含 abort、turn、step、wakeRequested。setPhase 只有对外 status 变化才发 agent/status。新 Agent 从 turnBoundary projection 的 lastTurn 起步。phase 与构造。

whenIdle 不只是 await 一次旧 Promise,而在 activityDone 身份变化时继续等下一活动;这样取消后 latched wake 导致新 driver 接力也被等待。kick 内 turn 级异常被 contain,最终回 idle;大部分失败通过 agent/error 和 turn/end 记录,不应假设 whenIdle 一定 reject 原 tool failure。另有 listener 本身抛错等更外层路径需要正常宿主监督。

7.4 一套 inbox,两个排队目标,三种公开送信方式

入口 目标 wakeup 意义
followup(message) next-turn true 每条一般拥有独立新 turn
steer(message) next-step true 活跃工作到下一 step 接收;idle 时也可启动
inject(message) next-step false 只排上下文,不单独唤醒
send(message,target,wakeup) 显式 显式 底层入口

发送先通过 inbox.splice 写 agent/inbox/spliced durable 事件,再考虑 wakeDriver。醒来的输入不能加入已经 aborted 的活动,send 在插入之前捕捉该条件,将目标改 next-turn,避免 splice observer 中可重入 cancel 改变分类。maintenance 或 abort 中 wake 会 latch,disposed 不 latch;idle wake 即使输入后来被 observer 清掉,也开启 turn boundary。发送和醒来。

inbox projection 由日志 splice 恢复,start/remove count 都校验为 valid range,两个队列合起来 pending message.id 不能重复。claim 总取全部 next-step;target=next-turn 时再追加 next-turn 第一条。因此 injected context 在本次 claim 的 queued prompt 前面,下一条 followup 留到下个 turn。claim removal 不表示 canceled;普通 remove/clear 的 discarded 事件才标取消。inbox.claim。

splice 接受普通数组 splice 风格负 start、Infinity 等,先 Math.trunc/clamp 成归一化 durable start/deleteCount。no-op 不写日志。replace/remove 按 pending message identity,已 claim 的输入不能再通过 inbox.remove 撤销,后面的 pre-step/abort 才处理 admission。

7.5 turn:完整执行顺序

flowchart TD W[唤醒 driver] --> O[turn/start] O --> C[claim next-step 加一条 next-turn] C --> A[assemble prompt 与 runtime context 候选] A --> P[agent/pre-step waterfall] P -->|reject| B[turn/end blocked;没有 step] P -->|首 enter 消息空| Z[turn/end completed;没有请求] P -->|enter| S[step/start;安装 tool recovery] S --> Q[step 的 attempt 循环] Q --> E[step/end finally] E -->|工具还欠请求或 next-step 有输入| C E -->|无欠请求且 next-step 空| T[agent/turn-stopping serial] T -->|监听器注入了 next-step| C T -->|仍然无 next-step| D[turn/end] D -->|还有 pending| O

turn/start 在 claim 与 prompt assembly 之前。preStep 先 claim,再 await SystemPrompt.assemble、renderContextSections、runtimeContext.project,最后 agent/pre-step waterfall;每个 async 窗口之后 signal.throwIfAborted。rejected input 已从 inbox 移走,插件若要留给后续需显式 restore,否则 blocked turn 不会自动重新排队。preStep。

turn 中 target 初始 next-turn,后续 next-step。decision reject 直接结束 blocked 并 return false;第一 decision enter 被改空,结束 completed 且不 step/start;已有 turnEnds 后再次 preStep 无新 messages 可以 break。只有真正 admitted step 才写 step/start 和增 phase.step。

stepEnd 的 null 表示 tool calls 正常完成后仍欠一次模型请求;completed/max-tokens 表示本步骤可以停止,但 next-step 输入或 stopping hook 可以继续。max-tokens 是 sticky:后续普通 completed 不降级 turn 结局。next-turn 队列不直接合并进活跃 turn,等本 turn 正常结束后才开下一 turn。取消/错误被 catch 退出 kick,已有 latch 或未来新 wake 再运行。完整 turn。

7.6 step:从 admission 到 attempt settlement

step 首先 render 一份 assembly system prompt,firstAttempt=true,进入 while。每次 attempt:prepareRequest→实际 preparedCall→systemPrompt.project→提交 system/message→仅首尝试提交 accepted user/messages→buildRequest→AssistantStreamAttempt→preparedCall.stream 或 llm.stream。

开始迭代前有 abort check,live.start 发生在拿到 stream 之后;每个 chunk 前 check,再 live.push;结束后再 check。实时 frame 的 attemptId/revision 是 process-local,不承诺跨 resume 唯一计数延续。attempt 主体。

成功 finish 的 message 含 content/source provider/model/replayState、可选 usage、精确 stream;先 settle assistant/message,才筛 tool-call blocks。finish max-tokens 直接返回,不分派该消息里的 tool calls;normal finish 无 tool-call 返回 completed,否则调 executeToolCalls。工具结果的 concludesTurn 聚合为 completed,否则 null 继续一请求。

7.7 两种错误,不要把所有 throw 都叫“可重试模型失败”

模型适配器最终选择/dispatch/iteration 的失败通常由 LLM seam 规范化为 terminal error/aborted finish chunk。loop 将这些 settle 成 assistant/attempt,调用 agent/request-error waterfall。listener 返回 {kind:'retry'} 且不 next 才继续 attempt;默认 undefined 变 LlmError,turn/error。retry policy 信息来自 preparedCall;loop 自己不会无条件指数退避。

middleware、result-processing、tool 调度或 listener 直接 throw 的异常通常走 step catch,不进入这条 terminal-finish retry 路径。流迭代开始后 direct throw 也先记录 attempt 或取消安全 prefix,再重抛;没有开始的 stream throw 不伪造 start/settlement。listener 不应吞掉 AbortSignal 或把插件错误无限伪装成模型重试。terminal retry。

retry 不重跑 assembly/preStep、不重复 user/messages;但 prepareRequest/buildRequest 每次做,配置/能力可变化,每次 request 各自 frozen,详见06。

7.8 cancel 的精确作用与限制

cancel 默认 clear inbox,非 idle 清 wakeRequested,再 abort active controller;keepInbox=true 保留队列。cause 仅 user/parent/hook(reason)/disposed;turn/end 复制允许字段,不存 fetch 给 live signal.reason 添加的 stack。disposed 通知拥有者退出,不重新 latch。

取消 preparation 时 input 已 claim,但还没有 system/user admission;日志有 inbox removal/turn-start/step boundary,model surface 不会加入被取消的 users。取消 live stream 若有安全 prefix 用 interrupted message;tool calls 不因为部分 JSON 就执行。工具取消是 cooperative,已开始的业务工作必须等 quiescence,代码不会丢掉 Promise 宣称退出。取消原因复制,cancel。

sequenceDiagram participant H as 宿主 cancel participant A as Agent participant T as 运行中的 Tool participant S as Session H->>A: cancel cause A->>T: AbortSignal T-->>A: cooperative work 已收敛 A->>S: started results 与 skipped error pairs A->>S: step/end A->>S: turn/end aborted A-->>H: whenIdle 收敛

如果工具不观察 signal 且永不结束,whenIdle/teardown 也可能卡住;timeout policy 是 cooperative wrapper,不能强杀 same-process JS。自己的耗时业务可以放在受监督 subprocess 或 worker 的 seam 中,明确 cancel 和 drain。

7.9 工具池:并发执行,结果仍按模型顺序

executeToolCalls 先把全部 raw JSON args parse;空 args='{}',非法 JSON 原 string 保留以便 INVALID_ARGS。executionMode 以 live definition 和有效 args 决定 parallel/exclusive。exclusive 形成独占 barrier;parallel 切到滚动 bounded pool,maxParallelToolCalls 默认常量由配置读取。完整 scheduler。

startCall 先 append tool/call,再 await prepare;pre-policy 是有序的,只有 dispatch/body Promise 重叠。slot 存待 finalization result;commitReady 仅推进连续已完成的模型顺序 slot,await post-policy→append tool/result→把 additionalContexts 进入 next-step→合并 concludesTurn。所以 B 工具先结束,不代表 B 的 post-policy 和日志结果先于 A。

每次后续开始前 re-read concurrency mode,工具 registry 改动能把还没开始的调用改成 exclusive barrier;已有请求 schema 不等于永久绑定工具实现。aborted 停止补 pool,drain started,按序提交,再给没 dispatch 的调用合成 call/result ABORTED_BEFORE_DISPATCH。concludesTurn 不等于立即丢掉同 message 其他工具;当前已请求的工具仍按 scheduler 流程处理。runGroup。

7.10 failed step 与缺失工具结果

turn 在每个 step 临时订阅 session/event 驱动 ToolCallRecovery。scheduler 内部错误停止新分派并等 started promises 后 reject;step catch 为每个已请求未回答调用记录 conservative error:没 call record 是 TOOL_NOT_STARTED,有 call record 是 TOOL_OUTCOME_UNKNOWN。已 committed result 保持不变。修复本身失败 AggregateError 保留原失败,finally step/end 仍尝试写。step recovery。

这里的“未知”不是“没做”:外部文件/数据库可能已改,只是结果未记录。后续 request 有 paired tool history,但没有自动安全重放所有副作用。恢复和 fork 同用 repair 算法,05讲原因文案的差异。

7.11 结束策略与自建 Agent 的插入点

agent/turn-stopping 是 serial,不带 next;可 inject 一个额外 next-step input 让本 turn 再走一次。但 serial 遇 bail 非空即停止余下 listener,纯观察者通常返回 void。检查 signal,再检查 inbox.nextStep,只有仍为空才结束。错误是 turn/end error,取消是 aborted,preStep拒绝是 blocked,正常是 completed,至少一次 tokens ceiling 是 max-tokens。

你的需求 优先机制
自己的固定 persona/tool set create.setup / preset scoped registrations
不满足条件就不发请求 agent/pre-step reject
动态模型 route agent/request wrapping/override
provider 错误的有限 retry agent/request-error policy
本轮收尾前复核 agent/turn-stopping + logged inject
文件操作后的下次 context tool exec.deferContext
一段长期目标自动续轮 goals + goal-round-driver,不改 kick
观察 UI stream agent/assistant-stream,持久结算仍读 Session

验证自己的 Agent 应使用脚本化 adapter,明确收到的 request、日志顺序、并发 begin/end、取消时 input 是否 admitted;每种顺序都应有反例。不要以文字“支持 retry/cancel”代替这些行为断言。