1. 从一次“消息发出去了但 Agent 没动”说起如果你正在读 openclaw 的运行时源码大概率会在agent-run-execution-phase.ts这个文件前卡住。它不像agent-run-loop那样有醒目的 LLM 调用和 Tool 循环也不像 handler 那样有清晰的阶段编号它更像一个“装配车间”把上游传下来的ingressOpts一层层补齐最后交给dispatchAgentRunFromGateway发射出去。问题是一旦这中间某个字段没填对表现就是消息已经入库、session 事件也广播了但 Agent 就是不启动日志里还看不到明显报错。agent-run-execution-phase.ts是 openclaw agent 运行时的第 ⑨ 阶段实现它只导出一个函数startAgentRunExecution。这个函数不执行 LLM也不跑工具它做的是执行前的最后组装拿准入锁、检查中止、恢复子 Agent session、广播 session 变更、标注消息来源、创建转录记录器、处理 exec 审批延续、解析插件工具授权然后把runContext和ingressOpts拼好调用dispatchAgentRunFromGateway真正发射。适合谁读适合已经能跑通 openclaw 基本对话、想搞清楚多 Agent 编组和 session 复用到底在哪一层生效的开发者。这篇会给出可复制的调用骨架、断点验证步骤以及怎么用 TaoToken 统一 Key/API 通道在本地复现这个执行阶段。2. 前置用 TaoToken 统一 Key 与 API 通道在本地复现执行阶段之前先把模型通道固定下来。openclaw 在ingressOpts里会带provider和model如果本地同时配了好几个供应商的 Key调试时很容易分不清这次 run 到底走了哪条通道。我的做法是统一走 TaoToken 的 API 入口这样provider字段和实际请求地址是一一对应的断点里看到什么就是什么。TaoToken 官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 。注意 API 地址后面不加 UTM 参数直接作为 base URL 用。你需要先在控制台创建一个 Key然后把它写进 openclaw 的 provider 配置里。# 环境变量方式避免把 Key 写进源码 export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api{ provider: taotoken, model: claude-sonnet-4-20250514, baseURL: https://taotoken.net/api, apiKeyEnv: TAOTOKEN_API_KEY }这样配置之后startAgentRunExecution组装出来的ingressOpts.provider就是taotokeningressOpts.model就是你指定的模型名。断点打在dispatchAgentRunFromGateway调用前直接看ingressOpts就能确认这次 run 用的是哪条通道不用再去翻多层配置合并逻辑。如果你要长期跑编码类 Agent 任务建议用 Coding Plan 而不是按次调用Key 管理更省心只是临时验证模型行为用模型对话页面手动发一条也能对照。但源码调试阶段还是本地环境变量加 API 基址最直接。3. 可复制配置startAgentRunExecution 调用骨架先把agent-run-execution-phase.ts的核心结构还原成一个可复制的骨架。下面这段不是源码原文而是按调用顺序整理出来的等价骨架方便你对照自己的版本打点。// agent-run-execution-phase.ts 核心调用骨架 export async function startAgentRunExecution(params: StartAgentRunExecutionParams) { // 层1gateway 级准入锁防止执行期间 gateway 关闭 const releaseGatewayRootContinuation retainGatewayRootWorkAdmissionContinuation(); try { // 层2session 级准入锁同一 session 同时只能有一个 run await params.prepared.activeGatewayWorkAdmission.run(async () { // 层3中止检查已 abort 直接返回 timeout if (params.prepared.activeRunAbort.controller.signal.aborted) { setAbortedAgentDedupeEntries({ /* ... */ }); params.respond(true, { runId: params.runId, status: timeout, summary: aborted, }); return; } // 层4子 Agent session 恢复多 Agent 场景关键 if (!params.isOneShotModelRun params.resolvedSessionKey) { await reactivateCompletedSubagentSession({ sessionKey: params.resolvedSessionKey, runId: params.runId, task: params.message, }); } // 层5session 变更事件广播 if (isNewSession) emitSessionsChanged(/* ... */, create); emitSessionsChanged(/* ... */, send); // 层6跨 session 消息来源标注 const annotatedMessage annotateInterSessionPromptText( params.message, params.inputProvenance, ); // 层7条件创建转录记录器 const userTurnTranscriptRecorder params.resolvedSessionKey /* 条件 */ ? createUserTurnTranscriptRecorder({ /* ... */ }) : undefined; // 层8exec 审批延续 const execApprovalFollowupRuntimeHandoff consumeExecApprovalFollowupRuntimeHandoff({ /* ... */ }); // 层9插件工具授权 const runtimePluginToolGrant params.client?.internal?.agentRunTracking plugin_subagent ? params.client.internal.runtimePluginToolGrant : undefined; // 层10组装 runContext const runContext { messageChannel, accountId, senderId, groupId, groupChannel, groupSpace, currentChannelId, currentThreadTs, }; // 层11组装 ingressOpts所有 LLM 调用需要的参数 const ingressOpts { message: annotatedMessage, images, imageOrder, agentId, provider, model, sessionId, sessionKey: params.resolvedSessionKey, thinking, deliver, deliveryTargetMode, channel, threadId, spawnedBy, groupId, groupChannel, groupSpace, toolsAllow, runtimePluginToolGrant, workspaceDir, cwd, userTurnTranscriptRecorder, abortSignal: params.prepared.activeRunAbort.controller.signal, lifecycleGeneration, // ... 其余字段 }; // 层12发射 dispatchAgentRunFromGateway({ ingressOpts, runId: params.runId, dedupeKeys, abortController: params.prepared.activeRunAbort.controller, cleanupAbortController, onSettled, respond: params.respond, context: params.context, taskTrackingMode, restoreAdmittedRecovery, }); }); } catch (err) { // 层13失败清理 setErrorDedupeEntries({ /* ... */ }); params.respond(false, { runId: params.runId, status: error, error: String(err) }); } finally { if (!dispatched) { restoreAdmittedRecoveryState(); releaseCronContinuation(); releaseMainRestartRecoveryOwner(); cleanupAdmittedRun(); } releaseGatewayRootContinuation(); } }这段骨架里有两个地方最容易看漏。第一是层 2 的activeGatewayWorkAdmission.run它是一个异步回调层 3 到层 12 全都在这个回调里面所以断点如果只打在函数入口是看不到ingressOpts组装过程的。第二是层 13 的finally里有个dispatched标志只有发射成功才会置位失败时所有锁和恢复状态都要回滚否则下一次 run 会被卡住。4. 验证请求断点打在哪、看什么光看骨架不够得实际跑一次。下面是我验证时用的断点位置和观察点按执行顺序排列。第一个断点打在startAgentRunExecution函数入口观察params里有没有resolvedSessionKey、isOneShotModelRun、inputProvenance这三个字段。如果resolvedSessionKey是空的层 4 的子 Agent session 恢复会被跳过多 Agent 场景下上下文就断了。第二个断点打在层 2 的activeGatewayWorkAdmission.run回调内部第一行确认准入锁已经拿到。如果这里一直不进入说明同一个 session 有另一个 run 还在跑锁没释放。第三个断点打在层 11 组装完ingressOpts之后、层 12 调用之前。这是最关键的一处直接打印console.log([exec-phase] ingressOpts, { provider: ingressOpts.provider, model: ingressOpts.model, sessionKey: ingressOpts.sessionKey, spawnedBy: ingressOpts.spawnedBy, groupId: ingressOpts.groupId, groupChannel: ingressOpts.groupChannel, groupSpace: ingressOpts.groupSpace, toolsAllow: ingressOpts.toolsAllow, workspaceDir: ingressOpts.workspaceDir, cwd: ingressOpts.cwd, });第四个断点打在dispatchAgentRunFromGateway内部第一行确认ingressOpts原样传进去了。如果这里看到的字段和第三处不一致说明 dispatch 层又做了一次字段过滤或重命名。验证成功的标志是dispatchAgentRunFromGateway返回后onSettled回调被触发session 事件里能看到send广播并且 Agent 循环开始产生 LLM 请求。用 TaoToken 通道时你可以在控制台的请求日志里看到对应的模型调用记录provider显示为taotoken模型名和ingressOpts.model一致。# 本地跑一次最小验证 OPENCLAW_LOG_LEVELdebug \ TAOTOKEN_API_KEYsk-你的key \ node ./scripts/run-agent-once.js \ --session-key test-session-001 \ --message 列出当前工作目录下的文件 \ --provider taotoken \ --model claude-sonnet-4-20250514跑完之后检查两处一是 debug 日志里有没有[exec-phase] ingressOpts这行二是 TaoToken 控制台有没有对应的请求记录。两处都对上说明从ingressOpts到startAgentRunExecution的发射链路是通的。5. 本篇常见错排查5.1 消息入库但 Agent 不启动日志无报错最常见的原因是层 3 的中止检查提前返回了。prepared.activeRunAbort.controller.signal.aborted为 true 时函数直接respond一个timeout状态就结束了不会走到层 12。排查方法是在层 3 的if里加一行日志打印aborted的原因和触发时间。如果确实是用户取消导致的那是正常行为如果是误触发检查上游是谁调用了abort()。5.2 多 Agent 场景下子 Agent 上下文丢失层 4 的reactivateCompletedSubagentSession只在!params.isOneShotModelRun params.resolvedSessionKey两个条件都满足时才执行。如果isOneShotModelRun是 true子 Agent session 不会被恢复新消息会当成全新 session 处理上下文自然就断了。检查params.isOneShotModelRun的来源确认它是不是被上游错误地设成了 true。5.3 ingressOpts 里 provider 和实际请求不一致层 11 组装ingressOpts时provider和model是从上游参数直接取的。如果上游传的是taotoken但实际请求打到了别的地址说明dispatchAgentRunFromGateway内部又做了一次 provider 解析。检查agent-run-dispatch.ts里有没有根据provider字段重新映射 base URL 的逻辑。用 TaoToken 时base URL 固定是https://taotoken.net/api不要在 dispatch 层再覆盖。5.4 锁泄漏导致后续 run 全部卡住层 13 的finally里只有dispatched为 false 时才执行回滚。如果dispatchAgentRunFromGateway抛异常但dispatched已经被置为 true回滚逻辑不会执行retainGatewayRootWorkAdmissionContinuation拿到的锁就不会释放。表现是后续所有 run 都卡在层 2 的activeGatewayWorkAdmission.run外面。排查方法是检查dispatchAgentRunFromGateway内部有没有在抛异常前错误地置位dispatched。5.5 session 事件广播重复层 5 里create和send是两个独立事件新建 session 时会先发create再发send已有 session 只发send。如果客户端收到重复的create检查isNewSession的判断逻辑是不是在并发场景下被算了两次。这个字段通常来自 session 存储的查询结果并发 run 时可能有竞态。6. 继续往下读从发射到 Agent 循环startAgentRunExecution的职责边界很清楚它不执行 LLM只负责把执行所需的一切准备好并发射出去。真正启动 Agent 循环的是dispatchAgentRunFromGateway它来自agent-run-dispatch.ts负责创建 Agent 进程或线程、开始 LLM 与 Tool 的闭循环。所以读完这篇之后下一步应该去读agent-run-dispatch.ts看ingressOpts是怎么被消费的。如果你在本地复现时想固定模型通道直接用 TaoToken 的 API 基址https://taotoken.net/api配provider: taotoken就行Key 在控制台创建后写进环境变量。长期跑编码类 Agent 任务的话Coding Plan 比按次调用更适合Key 和额度管理都在一个地方。调试过程中如果ingressOpts里的字段和实际请求对不上优先检查 dispatch 层有没有二次覆盖而不是回头改 execution-phase 的组装逻辑。