agent-run-handler.ts(531 行)— Agent 执行的 9 阶段总控流水线

agent-run-handler.ts 是所有 Agent 请求的统一入口。不管消息从哪个 channel 进来(网页、企微、Discord),最终都会流到这里。它的 9 个阶段像一条装配线。


逐阶段详解

① Preflight(预检)— agent-request-preflight.ts L35-62  

prepareAgentRequestPreflight({ params, respond, context, client })

if (!preflight) return;  // 请求不合法直接拦截。校验请求参数是否合法(runId、session、权限等),解析 20+ 个关键变量

对 ClawForge 注:ClawForge是我基于AgentTeam+OpenClaw+阿里云token plan做的一个面向软件研发全流程的多Agent协同代码审查与自愈系统的意义:这里是参数入口。如果 Task Decomposer 需要从请求里提取额外的元信息(比如 taskType: "pr" | "issue"),就在 Preflight 返回的变量L39-62里加字段

做的事:

  • 校验 params 的合法性(必填字段、格式)preflight.ts的L62-72
  • 提取 request(用户消息体)、cfg(配置快照)preflight.ts的L147,校验request的合法性。preflight.ts的L73
  • 生成 runId(每次执行的唯一 ID)preflight.ts的L148
  • 判断是否允许模型覆盖(allowModelOverride),判断与request对象中的是否可覆盖状态是否一致 preflight.ts的L90
  • 判断是否是 cron 延续运行(preflight.ts的L92)、是否是重启恢复、session返回的结果是否符合预期,如不符合抛出异常(preflight.ts的L93-105)  
  • 提取 dedupe keys(幂等键)(preflight.ts的L164-167) 
  • 提示词可持久化、获取缓存对象(preflight.ts的L168-200

返回值是关键变量的集合,这些变量贯穿后续所有阶段。

② Dedupe(去重/幂等)— agent-dedupe-lifecycle.ts

createAgentDedupeLifecycle({ cfg, request, runId, lifecycleGeneration, agentDedupeKeys, ... })

相同 runId 的请求只执行一次。如果 runId 已经处理过,直接返回之前的结果。防止同一请求被重复执行。返回三个操作:

  • reserve:预留一个 dedupe 槽位(占位) 
  • clearUnaccepted:清除未被接受的 dedupe 占用 
  • abortForLifecycleRotation:生命周期轮转时终止旧 run

对 ClawForge 的意义: 直接影响任务分发在多 Agent 场景下,同一个 PR/Issue 被分解成 4 个子任务发给 4 个 Worker。每个子任务需要自己的 runId,但父任务需要一个全局唯一 ID。需要在这里考虑幂等链——父任务的幂等和子任务的幂等是什么关系

③ Routing(路由)— agent-request-routing.ts

prepareAgentRequestRouting({ request, cfg, expectedSession, ... })

if (!routing) return;

决定消息发给哪个 Agent、哪个 Session。解析 toagentIdsessionKey 等路由信息。

  • 解析通过rpc协议提交到对话窗口的附件文件
  • 从参数配置中获取所有已知的agent
  • 解析 agentId(默认 agent 还是指定 agent),校验agentId是否存在agent列表中
  • 解析 requestedSessionKey(会话键), sessionId,  to(路由到哪个agent,收件人),校验sessionkey的格式
  • 设置明确的接收人会话explicitRecipientSession,如果没有明确的接收人,报异常
  • 校验sessionKey是否合法,如不合法报异常
  • 解析附件、收件人、是否为 raw model run
  • 以上校验都做完了,session key合法,respondUnavailableAgentSessionForKey返回true,则把重复的请求删掉
  • 预留 dedupe 槽位(调用 reserveDedupe
  • 返回routing对象

这是多 Agent 拆分的关键切入点——如果需要根据消息内容路由到不同 Agent,就在这里做。

对 ClawForge 的意义这是要动刀的地方之一。当前 Routing 是「消息 → 单个 Agent」。ClawForge 要做的是「消息 → Task Decomposer Agent」。需要在这里插入逻辑:

当前:Routing → 匹配单个 agentId

ClawForge:Routing → 匹配 Manager Agent(行为是"分析并分解任务")

④ Content(内容组装处理)— agent-content-phase.ts

prepareAgentContentPhase({ request, cfg, isRawModelRun, normalizedAttachments, ... })

if (!content) return;

解析消息内容——文本、附件、图片、引用等。把原始消息转成 Agent 能消费的格式。

构建发给 LLM 的消息体:

  • 处理图片、及图片附件顺序
  • 确定 replyTo(回复引用)
  • 确定接收频道/账户/线程
  • 如果 agentId 为空,自动选择一个
  • 返回 message(最终的文本消息体)

对 ClawForge 的意义和 Routing 紧密配合。Task Decomposer 拿到的输入就是从这里来的如果需要在分解任务时附带源码上下文、PR diff、文件列表,这里就是注入额外上下文的地方。

⑤ Reset(重置处理)— agent-reset-phase.ts

runAgentResetPhase({ request, cfg, requestedSessionKey, message, agentId, ... })

if (resetPhase.stop) return;

处理 /reset/clear 等重置命令。清空会话历史,重新开始。

处理 /reset 等特殊指令:

  • 如果消息是 /reset,清除会话上下文
  • resetPhase.accepted 标记是否接受了重置
  • resetPhase.stop 标记是否应该停止后续流程

⑥ Session(会话准备)— agent-session-prepare.ts + agent-session-persist.ts

这是最长的阶段(约 170 行),大约占了文件 1/3 的代码,分两步:

prepareAgentSession()           persistAgentSessionPhase()

       │                                              │

       ├─ 查找已有 session              ├─ 写入 session 到存储

       ├─ 或创建新 session              ├─ 合并 session patch

       ├─ 确定 canonicalKey            ├─ 处理 spawnedBy(子 Agent 来源)

       ├─ 确定 sessionAgentId         ├─ 设置 cron continuation claim

       ├─ 读取 storePath                  └─ 设置 main restart recovery lease

       └─ 构建 sessionPatch

关键变量:

  • canonicalSessionKey:规范化的 session 键
  • sessionAgentId:该 session 绑定的 agent
  • isNewSession:是否为新建 session
  • spawnedBy:谁 spawn 了这个 session(子 Agent 关系链)
  • groupId / groupChannel / groupSpace:多 Agent 分组信息

const preparedSession = prepareAgentSession({ ... });

// ... 大量的 session 元数据处理

const persistedSession = await persistAgentSessionPhase({ ... });

  • Prepare:加载或创建 Session(会话历史、配置、状态)
  • Persist:持久化 Session 到磁盘(包括 session 的元数据、维护配置)

对 ClawForge 的意义多 Agent 会话隔离的关键。每个 Worker 需要独立 Session,Manager 需要一个编排 Session。当前代码通过 normalizedSpawned.groupIdresolvedGroupIdresolvedGroupChannelresolvedGroupSpace 这些变量已经预留了多 Agent 的会话基础设施

groupId    → 同一次运行的 4 个 Worker 共享同一个 groupId

groupSpace → 共享文件空间(clawforge的MinIO 路径就是拼这个)

这些字段已经存在于代码里,是要用起来的

⑦ Admission(准入/并发控制)— agent-admission-controller.ts

createAgentAdmissionController({ cfg, runId, lifecycleGeneration, ... })

await acquireGatewayWorkAdmission(storePath ?? `agent:${sessionAgentId}`);

同一 Session 同时只能有一个 run 在执行。这是一个排他锁——后面的请求要排队等前面的完成。

  • acquireGatewayWorkAdmission(storePath):申请入场许可
  • assertGatewayWorkAdmissionAllowed:断言许可有效
  • respondToGatewayAdmissionOutcome:如果有排队等待,通知客户端

设计意图类似"锁",防止两个并发请求同时修改同一个 session 的状态。

对 ClawForge 的意义这是 ClawForge 调度器的战场。当前 Admission 是「按 Session 串行」,但 ClawForge 需要:

当前:1 Session = 1 Lock = 串行执行

ClawForge:1 Manager Session + N Worker Sessions = Manager 排队 + Workers 并行

因为每个 Worker 是独立 Session,Admission 锁天然隔离——不需要改 Admission 逻辑本身,只需要保证 Manager 把任务分发给不同 Session 的 Worker 就行。这就是为什么要在 Dispatch 阶段(下一步)动手,而不是在这里。

⑧ Delivery(投递设置)— agent-delivery-phase.ts

resolveAgentDeliveryPhase({ request, cfg, sessionEntry, resolvedSessionKey, ... })

if (!delivery) return;

决定最终结果投递到哪个 channel、哪个目标,确定回复发到哪里

  • 解析 channel、account、thread
  • 确定 bestEffortDeliver 策略
  • 返回 activeSessionAgentId

对 ClawForge 的意义:在 ClawForge 里,每个 Worker 的输出不是直接投递给用户的,而是投递回 Manager 做聚合Delivery 阶段需要支持「投递到 Matrix Room」而不是直接投递给用户 channel。这可能需要微调,但初期可以用 Matrix Room 的消息机制替代

⑨  Dispatch/Execute(分发+执行)— agent-run-admission-phase.ts + agent-run-execution-phase.ts

const preparedDispatch = await prepareAgentRunDispatch({...});

if (!preparedDispatch) return;

startAgentRunExecution({...});  // 注意:不 await! 

// 注意:执行阶段是 fire-and-forget,不阻塞主流程

gatewayAdmissionTransferred = true;   // �� Admission 锁在这转移

startAgentRunExecution({             // �� 进入 run-loop

  prepared: preparedDispatch,

  ...

});

  • Dispatch:最后一道准入检查 + 组装执行参数(ingressOpts
  • Execute:调用 commands/agent.ts → PI Embedded Runner → 进入 run-loop(LLM↔Tool 闭循环)

分两步:

  1. prepareAgentRunDispatch:做最后的准备(模型选择、系统提示词装配等)
  2. startAgentRunExecution异步启动,不阻塞主流程

第 9 阶段是 fire-and-forget——主 handler 立即返回,Agent 的实际执行(LLM 调用、工具循环)在后台异步进行。执行结果通过 WebSocket 推送给客户端。

9 个阶段,每个阶段都有机会短路返回。现在看最后一步 startAgentRunExecution 连接到哪里——这是真正跑 LLM 调用的地方

    对 ClawForge 的意义这是核心插入点

    当前逻辑:

    Dispatch → 组装 ingressOpts → Execute → run-loop → 回复

    ClawForge 要变成:

    Dispatch → Task Decomposer(新)→

      ├─ Agent A Execute → run-loop → MinIO

      ├─ Agent B Execute → run-loop → MinIO

      ├─ Agent C Execute → run-loop → MinIO

      └─ Agent D Execute → run-loop → MinIO

    → Aggregator → 回复

    具体改动思路:

    1. 在 prepareAgentRunDispatch 返回后,不直接 startAgentRunExecution
    2. 先让 Manager Agent 跑一次 run-loop,产出任务分解结果
    3. 对每个子任务调用 sessions_spawn 创建 Worker,Worker 各自跑完整 agent-run-handler(因为 Worker 也是标准 Agent)
    4. Aggregator 等待所有 Worker 完成,汇总结果

    finally 块:清理与恢复

    releaseGatewayAdmission()             // 释放 session 锁

    releaseCronContinuationClaimWithRecovery()  // 释放 cron 占用

    clearUnacceptedAgentDedupe()          // 清理未完成的 dedupe

    scheduleMainSessionRecoveryPendingTarget()  // 如有需要,调度恢复

    注意 gatewayAdmissionTransferred 这个开关:如果执行阶段(⑨)成功启动,admission 的控制权就转移给了执行阶段,这里不再释放——由执行阶段完成后释放。


     ClawForge 切入点分析:

    阶段

    用途

    怎么做

    ③ Routing

    主任务拆分

    识别消息需要多 Agent 协作 → 修改 routing 逻辑,生成子任务

    ⑥ Session

    子 Agent spawn

    spawnedBygroupId 字段建立父子关系链

    ⑨ Execution

    子 Agent 调度

    在这里 sessions_spawn 出子 Agent,或修改 dispatch 逻辑

    这三个阶段是核心三明治:Routing 拆任务 → Session 建关系 → Execution 调子 Agent

    位置

    做什么

    ClawForge 改什么

    ③ Routing

    消息→Agent

    路由到 Manager Agent(Task Decomposer)

    ⑦ Admission

    串行锁

    不改——Worker 各自独立 Session,天然并行

    ⑨ Dispatch→Execute

    单 Agent 执行

    插入任务分解 + 多 Worker 分发 + 结果聚合

    更多推荐