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:完整执行顺序¶
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。
如果工具不观察 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”代替这些行为断言。