跳到主要内容

数据截至 (上游 commit dad6f5196773)

从聊天到 7×24 运营:多 agent 编排、任务调度与对话树渲染

30 秒导读: 前五章讲的是「一个 agent 怎么跑完一轮」。这一章讲 LobeHub v2 在那之上加的一层:让一群 agent 互相调度、让 agent 在没人看着的时候按点上工、让它的进展变成可订阅的事件,最后让前端把一堆扁平消息还原成一棵对话树渲染出来。


1. 这是什么(零基础也能懂)

一句话定义: 这一层把「聊天窗口」升级成「agent 运营平台」——agent 从「你问它答」的工具,变成有排班、有工单、有交班简报的员工

它解决的是三个具体的落差:

聊天产品做得到运营平台还需要什么
一个 agent 回一句话一群 agent 里,谁该说话、谁该干活,由一个 supervisor 决定
你点一次它跑一次它自己按 cron / 心跳跑,跑完给你一张简报
消息一条条往下排消息其实是一棵树(分支、并行、子 agent、工具回调),得还原出来才能渲染

用起来什么样(场景化): 你建一个群,里面有「调研 agent」「写作 agent」「校对 agent」。你说一句「帮我把这周的竞品动态整理成周报」。群主管(supervisor)先派调研 agent 去查(异步任务),查完自动叫写作 agent 起草,再广播给校对 agent 和你自己评审。你关掉浏览器,它照跑;第二天早上你看到的是一张 brief 卡片:标题、摘要、以及几个可以点的按钮。

一句话直觉: 把第 1 章的 AgentRuntime 想成一个人的工作循环,这一章就是把同一套循环再套一层到一个团队上——只不过这次「大脑」不是模型对 tool 的选择,而是 supervisor 对「让谁上」的选择。

本节不出现代码。往下才进机制。


2. 顶层全景(它大概怎么转)

这一章覆盖五个彼此独立、但串成一条线的子系统。先看它们的位置:

┌──────────────────────────────────────────────┐
人 / 定时器 ──► │ ① 编排层 supervisor 状态机 + executor │
│ packages/agent-runtime/groupOrchestration │
└───────────────────┬──────────────────────┘
│ 派活

┌──────────────────────────────────────────────┐
│ ② 员工层 builtin-agents / agent-manager │
│ 同构子 agent · 客户端子 agent · 异构 CLI │
└───────────────────┬──────────────────────────┘
│ 留痕

┌──────────────────────────────────────────────┐
│ ③ 数据层 operations / tasks / briefs / cron │
│ packages/database + apps/server/services │
└───────────────────┬──────────────────────────┘
│ 进展变事件

┌──────────────────────────────────────────────┐
│ ④ 信号层 packages/agent-signal │
└───────────────────┬──────────────────────────┘
│ 消息落库后

┌──────────────────────────────────────────────┐
│ ⑤ 渲染层 packages/conversation-flow │
│ 扁平 messages[] → ContextTree + FlatList │
└──────────────────────────────────────────────┘

各部件一句话职责:

部件干什么在哪个包/目录
GroupOrchestrationSupervisor群体编排的纯状态机:收结果、出指令packages/agent-runtime/src/groupOrchestration/
GroupOrchestrationRuntime把 supervisor 和 executor 拧成循环同上
builtin-agents12 个「内置岗位」的人设 + 工具包定义packages/builtin-agents/src/agents/
AgentManagerRuntimeagent 的 HR 系统:增删改查、装插件、换模型packages/agent-manager-runtime/src/
heterogeneous-agents接管 Claude Code / Codex 这类外部 CLI agent 的事件流packages/heterogeneous-agents/src/
tasks / briefs / cron 表工单、交班简报、排班表packages/database/src/schemas/
task* 系列服务跑工单、排依赖、定时唤醒、生成简报apps/server/src/services/
agent-signalsource → signal → action 三段式事件总线packages/agent-signal/src/
conversation-flow扁平消息 → 对话树 + 虚拟列表packages/conversation-flow/src/

主线走一遍(高层): 用户/定时器触发 → supervisor 决定派谁 → 被派的 agent 各自跑一次 AgentRuntime(第 1 章)→ 每次跑都在 agent_operations 记一笔账 → 完成时发 signal、写 brief → 消息落库 → 前端 parse() 把它们还原成树来渲染。


3. 群体编排:同一套「大脑/引擎」的二次套用

3.1 先说结论:这就是第 1 章的架构再来一遍

第 1 章讲过 LobeHub 的核心分工:Agent(大脑)只做决策、返回一条 Instruction;Runtime(引擎)只负责执行、返回一个 Result。 两者靠一个 while 循环互喂。

群体编排把同一个形状换了一层语义:

第 1 章:单 agent本章:agent 群体
大脑GeneralChatAgent.runner()GroupOrchestrationSupervisor.decide()
大脑吃什么AgentRuntimeContext(上一步 phase)ExecutorResult(上一步结果)
大脑吐什么AgentInstruction(call_llm / call_tool …)SupervisorInstruction(call_agent / delegate …)
引擎AgentRuntime 的 executorsGroupOrchestrationRuntime 的 executors
终止条件type: 'finish'type: 'finish'

循环长这样:

ExecutorResult ──►┌────────────────────────────┐
│ Supervisor(大脑) │ 纯函数,不碰 I/O
│ decide(result, state) │
└─────────────┬──────────────┘
│ SupervisorInstruction

┌────────────────────────────┐
◄─────────────────┤ Executor(引擎) │ 真的去调 agent / 写消息
ExecutorResult └────────────────────────────┘
循环直到 instruction.type === 'finish'

GroupOrchestrationRuntime.step() 就是这个循环的一步(GroupOrchestrationRuntime.ts:47):它先给 stepCount 加一并检查 maxSteps,再问 supervisor 要指令,遇到 finish 就把状态置 done,否则按 instruction.type 在 executors 表里查一个函数执行——查不到直接抛错(GroupOrchestrationRuntime.ts:89),没有兜底。

3.2 相位转移表(decide 的全部分支)

GroupOrchestrationSupervisor.decide()(GroupOrchestrationSupervisor.ts:48)是一个两层 switch。第一层按 result.type,第二层按 supervisor_decided 里的 decision。完整转移如下:

上一步结果附加条件下一条指令说明
initcall_supervisor同时把 round 归零、skipCallSupervisor 归 false
supervisor_decideddecision: 'speak'call_agent让某一个 agent 发言
supervisor_decideddecision: 'broadcast'parallel_call_agents并行发言,并强制 disableTools: true
supervisor_decideddecision: 'delegate'delegate把话语权交出去,supervisor 退场
supervisor_decideddecision: 'execute_task' + runInClientexec_client_async_task任务需要本地文件/shell,跑在桌面端
supervisor_decideddecision: 'execute_task'exec_async_task默认跑服务端
supervisor_decideddecision: 'execute_tasks'batch_exec_async_tasks一批任务并行
supervisor_decideddecision: 'finish'finishreason 取 params.reason,缺省 supervisor_finished
supervisor_decided未知 decisionfinishreason = unknown_decision: X
agent_spoke / agents_broadcasted / task_completed / tasks_completedskipCallSupervisorfinishreason = skip_call_supervisor
同上四种++round >= maxRoundsfinishreason = max_rounds_exceeded
同上四种其余call_supervisor带上新的 round
delegatedfinishreason = delegated_to_<agentId>
其它finishreason = unknown_result_type

三个容易读漏的细节:

  • round 只在「动作做完」时加一,不在 supervisor 做决策时加(GroupOrchestrationSupervisor.ts:161)。所以 maxRounds 数的是「派活轮次」,不是「LLM 调用次数」。
  • broadcast 硬编码禁工具。代码注释写得很直白:广播出去的 agent 默认不该调工具(GroupOrchestrationSupervisor.ts:82-83),因为那是「大家表个态」而不是「大家各干各的」。
  • skipCallSupervisor 是一次性开关:它在每次 supervisor_decided 时被覆写(GroupOrchestrationSupervisor.ts:65),在 init 时被清零。语义是「这一步做完就收工,别再回去问主管」——用在用户明确点名某个 agent 的场景。

3.3 SupervisorInstruction 家族

types.ts 里 8 个指令类型,按「做什么」分三组:

指令payload 关键字段定义位置
问主管call_supervisorroundsupervisorAgentIdtypes.ts:9
让人说话call_agentagentIdinstructiontypes.ts:21
parallel_call_agentsagentIdsdisableToolstoolMessageIdtypes.ts:32
delegateagentIdreasontypes.ts:101
让人干活exec_async_taskagentIdinstructiontitletimeouttypes.ts:53
exec_client_async_task同上(桌面端执行)types.ts:69
batch_exec_async_taskstasks[]types.ts:84
收工finishreasontypes.ts:112

反方向的 ExecutorResult 是 7 个(types.ts:244),一一对应:init / supervisor_decided / agent_spoke / agents_broadcasted / task_completed / tasks_completed / delegated

注意 parallel_call_agentsexec_async_task 都带 toolMessageId。这不是装饰:它是消息树的 parentId,决定了这批并行回复在前端会被渲染成一个「圆桌」块而不是散落的独立消息——第 8 节会接上这根线。

3.4 supervisor_decided 是谁产生的?靠 tool 的 stop: true

这是整个设计里最巧的一处。supervisor 不是用某个专用协议表达意图的,它就是一个普通 agent,只不过挂了 lobe-group-management 工具。它「决定让 A 发言」的方式,是调用 speak 这个工具

工具执行器做两件事(packages/builtin-tool-group-management/src/executor.ts:32):

// 真实源码节选,executor.ts:33-56
ctx.registerAfterCompletion(() =>
ctx.groupOrchestration!.triggerSpeak({ agentId: params.agentId, ... }),
);
return { content: `Triggered agent "${params.agentId}" to respond.`, state: { ... }, stop: true, success: true };

stop: true 让当前这轮 AgentRuntime 停下(supervisor 说完了);registerAfterCompletion 注册的回调在 runtime 完全收尾之后才触发编排——注释点明了原因:避免和消息写库产生竞态(executor.ts:33-34)。

工具清单在 manifest.ts,对外暴露 speak / broadcast / executeAgentTask / executeAgentTasks / vote;delegate / interrupt / summarize / createWorkflow 在 manifest 里是注释掉的(manifest.ts:68181198222)——类型和 executor 都写好了,但没给模型看。这是很典型的「代码先行、能力后开」。

3.5 一个坑:init 这条路在生产里其实没人走

GroupOrchestrationRuntime.run()(GroupOrchestrationRuntime.ts:109)会用 { type: 'init' } 起头。但客户端真正的入口是 store 里的 internal_execGroupOrchestration(src/store/chat/slices/aiAgent/actions/groupOrchestration.ts:255),它手写了循环、并且用 supervisor_decided 直接开头(见 triggerExecuteTask,同文件 :210-219)。

手写循环的理由是要在每步之间检查 operation 是否被用户取消(groupOrchestration.ts:326-331),run() 只在末尾查 abortController。全仓搜索 type: 'init' 在 groupOrchestration 语境下只有 GroupOrchestrationRuntime.ts:113 一处——也就是说 init → call_supervisor 这条转移目前只有测试和 run() 会碰到。

另一个诚实的边界:maxRounds 在客户端是硬编码 10(groupOrchestration.ts:21DEFAULT_MAX_ROUNDS),那段代码注释写着「2. Get Group Configuration」,但实际并没有读群配置。

3.6 服务端是另一套实现

客户端用 GroupOrchestrationRuntime 驱动循环;服务端根本不用它apps/server/src/services/toolExecution/serverRuntimes/groupManagement.ts 的头注释把差别说得很清楚:每个 group 动作返回 deferred: true,agent runtime 把 supervisor 挂起成 waiting_for_async_tool,等成员完成的 barrier 过了再唤醒它——supervisor 自己的 operation 就是编排循环,不需要单独的 driver(groupManagement.ts:16-17)。

客户端(浏览器/桌面)服务端(QStash 持久化)
循环驱动者internal_execGroupOrchestration 手写 whilesupervisor 自身的 operation
工具返回stop: true + afterCompletion 回调deferred: true
挂起方式内存里等 Promiseoperation 状态 waiting_for_async_tool
崩溃后丢失可恢复

这正是第 5 章讲的「同一条指令流跑在不同执行面」在编排层的体现:指令语义一致,驱动机制按执行面重写。


4. 子 agent 的三条路径

「让另一个 agent 去干活」在这个仓库里有三种实现,它们的差别不在语义而在谁在跑、事件从哪来

┌── 同构子 agent ── 服务端起一个 LobeHub agent(同一套 runtime)
一个 agent 想派活 ─┼── 客户端子 agent ── 桌面端起一个 LobeHub agent(能碰本地文件/shell)
└── 异构子 agent ── 外部 CLI(Claude Code / Codex)吐事件流,我们只做归档

4.1 同构子 agent:exec_sub_agent / exec_sub_agents

这是最直白的一条:子 agent 和父 agent 用同一套 AgentRuntime,只是换个 operation、换个 thread。

任务的形状是 SubAgentTask(packages/agent-runtime/src/types/instruction.ts:160):

字段作用
description给 UI 看的一句话
instruction给模型看的完整指令
inheritMessages要不要继承父对话的上下文
runInClient是否强制在桌面端跑(需要本地工具时必须为 true)
timeout毫秒,默认 30 分钟

回来的结果是 SubAgentResultPayload(instruction.ts:191),关键在它带 threadIdtaskMessageId——子 agent 的产出是独立的一条 thread,只把结论挂回父消息。

指令是怎么冒出来的?看 GeneralChatAgenttool_result 分支(packages/agent-runtime/src/agents/GeneralChatAgent.ts:734-760):当工具返回 stop: truedata.state.type === 'execSubAgent',大脑就把它翻译成 exec_sub_agent 指令。四种 stateType 对应四条指令:

工具返回的 stateType翻译成的指令跑在哪
execSubAgentexec_sub_agent服务端,单个
execSubAgentsexec_sub_agents服务端,多个
execClientSubAgent客户端执行面自带 execClientSubAgent 指令类型桌面端,单个
execClientSubAgents同上,批量形态桌面端,多个

子任务全部完成后,sub_agents_batch_result 分支会塞一条虚拟 user 消息再回到 LLM(GeneralChatAgent.ts:869-874)。注释解释了原因:某些模型(点名 Kimi K2)看到最后一条是任务结果时会返回空 content,以为活已经干完了。这是很典型的「为具体模型缺陷打的补丁」。

4.2 客户端子 agent:同一条指令,换个执行面

客户端子 agent 的 payload 与服务端 exec_sub_agent(s)(instruction.ts:314 / :324)结构同构,差别只在 executor 注册在哪一侧(上游已把 exec_client_sub_agent 指令类型合并:客户端执行面在 streamingExecutor.ts:1021execClientSubAgent 派发)。SubAgentTask.runInClient 的注释写明了取舍:非桌面平台上这个标记被忽略,一律回落服务端(instruction.ts:175-186)。

同样的模式在群体编排里也有:execute_taskrunInClient 就变成 exec_client_async_task(GroupOrchestrationSupervisor.ts:111-116)。一个布尔值决定指令类型,而不是决定执行分支——这样执行面的差异被挡在类型系统里,而不是散在 if 里。

4.3 异构子 agent:把外部 CLI 的事件流「归档」成消息树

Claude Code 和 Codex 不用我们的 runtime,它们吐自己的流。packages/heterogeneous-agents/ 要解决的是:怎么把这条外来的流,原样落成 LobeHub 的消息树。

核心设计是一对纯 reducer:

外部 CLI 事件流


getEventScope(event) 看 data.subagent.parentToolCallId 有没有

┌────┴────┐
▼ ▼
main 域 subagent 域
│ │
▼ ▼
reduceMainAgent ──委托──► reduceSubagentRuns
│ │
└────────► Intent[] ◄──────────┘


各环境的 interpreter 去真的写库 / 推 store

三个要点:

  1. reducer 不做 I/O。 reduce(state, event, ctx) 返回下一个 state 和一串 Intent;调用方先执行 intent、成功了才提交 state(commit-on-success)。所以一个抛错的 intent 会让这次 run 原地不动,重试时对着原 state 重放(mainAgentCoordinator/reducer.ts:14-19)。
  2. id 全部预分配。 reducer 通过 ctx.newId 提前生成消息 id,于是 intent 里可以直接带上具体的 parentId 链,不需要「先建再回填」的反向依赖(subagentCoordinator/types.ts:19-21)。
  3. 它是被合并出来的。 subagentCoordinator/types.ts:6-14 的注释说明:渲染端 executor 和服务端持久化 handler 曾经各自手写了同一个状态机,「几乎每个 hetero subagent bug 的震中都在那份重复里」。

getEventScope(subagentCoordinator/getEventScope.ts:14)只有 7 行,但它是整个分流的闸门:adapter 在每个来自子 agent 的事件上盖一个 data.subagent 戳,主 agent 的事件没有。

最烧脑的是挂载规则computeTurnParentId(mainAgentCoordinator/reducer.ts:115)决定下一轮 assistant 挂在哪:

这一轮是什么挂到哪前端渲染成
普通轮次lastSpineMessageId(最近一条非 tool 的主干消息)主干上的一段对话
信号触发的反应轮(如 Monitor 推 stdout)lastToolMsgIdEver(最近一条 tool 消息)挂在工具下的回调块

而且信号轮不推进主干(reducer.ts:158-160)——下一条正常消息会重新挂回信号轮之前的那条 assistant。这条规则和第 8 节的 SignalCallbacksNode 是同一件事的两端:写入侧决定 parentId,读取侧据此还原成折叠块。


5. agent 作为「被雇佣的员工」

5.1 AgentManagerRuntime:agent 的 HR 系统

packages/agent-manager-runtime/src/AgentManagerRuntime.ts:81 是一个 1200 行的门面类,把「一个 agent 能被怎么改」全部收口。它的方法就是一张岗位管理清单:

方法干什么行号
createAgent建一个新 agent:87
updateAgentConfig改配置:134
getAgentDetail查详情(给编排者判断「这人能不能干这活」):297
duplicateAgent复制一个:366
searchAgents搜市场/本地:396
getAvailableModels能挑哪些模型:497
updatePrompt改系统提示词(内部还有流式版 streamUpdatePrompt):546
installPlugin装工具(分 Composio / LobeHub skill / 市场插件三条路):665

关键点是它被 agent 自己调用agent-builder 这个内置 agent 挂的就是这套工具——也就是说,「造 agent」本身是一个 agent 的工作。

5.2 heteroAgentDescriptor:给外部 CLI 写一份「招聘启事」

getAgentDetail 有个专门的麻烦:如果目标是一个异构 agent(Claude Code / Codex),它的 model / provider / plugins 字段是误导性的——那些 CLI 自带工具集,根本不看这些设置。

packages/agent-manager-runtime/src/heteroAgentDescriptor.ts 就是为此存在的:它把配置映射成一段写给编排者 LLM 看的能力描述

HETERO_PROFILES(:42)硬编码了 6 种外部 runtime——amp / claude-code / codex / hermes / opencode / openclaw,每种带 capabilities 标签数组和一段自然语言 description。渲染出来的第一行长这样(renderHeteroRuntimeLines,:144):

Runtime: Claude Code (heterogeneous \claude-code` agent — ignores the chat model/plugins settings above)`

EXECUTION_TARGET_DESCRIPTIONS(:87)再补一句「跑在哪」:local(桌面端进程内)/ device(lh connect 连的具体设备)/ sandbox(服务端云沙箱)/ auto / none

妙在:这是把「机器可读的配置」翻译成「模型可读的岗位说明」的一次显式转换,而不是指望模型自己从 type: 'claude-code' 猜出它能改文件。

5.3 内置 agent 花名册

packages/builtin-agents/src/agents/ 下 12 个岗位,BUILTIN_AGENT_SLUGS(types.ts:12)是它们的编号表。每个定义是 { slug, avatar?, persist?, runtime(ctx) }:persist 是要落库的默认值(模型、chatConfig),runtime 是每次运行时动态算的 { systemRole, plugins, chatConfig }

slug岗位挂的关键工具 / 状态
inbox默认助手,不指定模型(用用户默认值)
group-supervisor群主管,第 3 节的那个 supervisorGroupManagement + GroupAgentBuilder,并关掉历史条数限制
group-agent-builder配置群设置、增删群成员GroupAgentBuilder
agent-builder造 agent / 改 agent主动剔除冲突工具(见下)
task-agent跑工单TaskIdentifier
verify-agent交付验收只有 VerifyToolIdentifier + 运行时注入的调查工具
page-agent文档编辑助手
web-onboarding网页端引导WebOnboarding
skill-management把用户反馈沉淀成可复用 skillskill 读 + createSkillIfAbsent / replaceSkillContentCAS
self-reflection一轮结束后自省,写记忆或记下待办工具面待后续 PR 注册
self-feedback-intent处理别的 agent 声明的自我反馈意图工具面待后续 PR 注册
nightly-review每天按用户本地时间跑一次,做安全的资源整理工具面待后续 PR 注册,当前无工具、运行即空转

两个值得抄的细节:

  • agent-builder 的排他清单。 它开头列了一串「同时存在就会打架」的工具并从 ctx.plugins 里剥掉(agents/agent-builder/index.ts:9-23)。原因写得很具体:lobe-agent-management 带的 <self_management> 提示词里 <current_agent> 指向 builder 自己,于是一句含糊的「帮我改一下…」会去改 builder 本身,而不是左边那个正在配的 agent。
  • verify-agenttoolMode: 'custom' 它明确关掉默认工具集和 agent 模式(agents/verify-agent/index.ts:12-24),注释说白了:让它「判定并提交」,而不是「四处溜达」。

三个 self-* agent 和 nightly-review 都标注「工具面在后续 PR 注册,现在调用是设计上的空转」。这是仓库当前的真实状态,不是我推断的。

5.4 agent-templates:员工的「入职材料」

packages/agent-templates/ 管的是另一件事:agent 的文档,以及这些文档什么时候被注入上下文。

DocumentTemplate(template.ts:12)的关键三个字段:

字段取值来源决定什么
loadPositionDocumentLoadPosition(types.ts:4)插在上下文的哪个位置:BEFORE_SYSTEM / SYSTEM_APPEND / AFTER_FIRST_USER / ON_DEMAND
loadRulesDocumentLoadRule(types.ts:20)什么条件下才加载:ALWAYS / BY_KEYWORDS / BY_REGEXP / BY_TIME_RANGE
policyLoadPolicyLoad(types.ts:37)全量注入还是渐进式披露(PROGRESSIVE)

目前只有一套模板集 CLAW_POLICY(templates/claw/index.ts:10),四份文档:AGENTS.md(BEFORE_SYSTEM)、IDENTITY.md / SOUL.md / BOOTSTRAP.md(都是 SYSTEM_APPEND)。这几个字段最终是被第 2 章的上下文流水线消费的。


6. 运营数据模型:让 agent 7×24 转起来

6.1 四张核心表,四种时间尺度

触发源 一次执行(秒~分) 长期(天~月)
────────── ──────────────── ──────────────
用户发消息 ─┐
cron 到点 ─┼─► agent_operations ◄── 一次 run 的账本(成本/token/步数)
heartbeat ─┤ │
signal ─┘ │ task_topics.operation_id

task_topics ──► tasks(工单;可嵌套 parentTaskId、可依赖)
│ │
└──► briefs ◄─────┘ 交班简报(给人看、给人点)

四种尺度的分工:

一行代表生命周期
agent_operationsagent 跑了一次秒~分钟
task_topics一个工单的第 N 次开工一次 operation
tasks一张长期工单天~月
briefs一次交班汇报直到人处理掉

6.2 operation:一次执行的账本

agentOperations(packages/database/src/schemas/agentOperations.ts:54)的状态机有 7 个值(:11-19):

状态含义
idle建了还没开始
running跑着
waiting_for_human卡在人工审批
waiting_for_async_tool卡在异步工具/子 agent(第 3.6 节 supervisor 挂起用的就是这个)
done / error / interrupted三种终态

配套的 completionReason 有 7 种(:21-29),多出 max_stepscost_limit 两种「被规则叫停」。

几个设计取舍值得记:

  • userId 故意不是外键(:76-80):operation 是审计资产,用户删了它也得留着。
  • 成本字段全部可空(:120-121):NULL 表示「还没测」,这样在途的行不会以「0 美元 / 0 token」的身份污染 SUM/AVG。
  • parentOperationId 自引用(:89-90):子 agent 派生出的 operation 挂在父的下面。
  • 完整轨迹不落库,只存 S3 key(traceS3Key,:156-157)。

verifyStatus / verifyPlan 那几列已标 @deprecated,注释说明它们搬到了 verify_runs,留着只是为了不对这张分析表做 ALTER(:96-101)——上游正在迁移中,引用时要注意。

6.3 task:工单与它的一堆卫星表

tasks(task.ts:25)是这一层的主表。除了常规的 assignee / status / priority,有三组特殊字段:

字段说明
自动化automationMode('heartbeat' | 'schedule',:70)两种模式互斥;null = 不自动化
心跳heartbeatInterval / heartbeatTimeout / lastHeartbeatAt(:73-75)timeout 默认关
定时schedulePattern / scheduleTimezone(:78-79)cron 表达式
话题额度totalTopics / maxTopics / currentTopicId(:82-84)限制一个工单最多开几次工

instruction 之外还存了一份 editorData(Lexical 富文本 JSON,:61),因为 markdown 会丢图片尺寸这类信息。

五张卫星表:

一行是什么关键字段
taskDependencies(:124)一条依赖边type: 'blocks' | 'relates';condition 预留条件依赖
taskDocuments(:162)钉在工单上的文档pinnedBy: 'agent' | 'user' | 'system'
taskTopics(:196)一次开工operationIdhandoff(交接摘要)、reviewScore 等评审结果
briefs(:242)一张简报卡type(decision/result/insight/error)、priorityactions(给人点的按钮)、resolvedAction
taskComments(:297)工单下的一条讨论作者可以是人也可以是 agent(两个可空外键)

handoff 是这套设计的精华:每次开工结束由 LLM 总结出 { title, summary, keyFindings[], nextAction }(:214-216),下一次开工的 prompt 就从这里接上——用一段结构化摘要代替「把上次全部消息重放一遍」。

一个已知的文档漂移:task.ts:64 的注释列了 6 个状态,但 TaskStatus 实际有 7 个(packages/types/src/task/index.ts:7-8),多一个 scheduled,而且 TaskLifecycleService 确实会写它(taskLifecycle/index.ts:277)。注释是旧的。

6.4 排班与技能:cron 和 skill

agentCronJobs(agentCronJob.ts:17)是「一个 agent 可以有多张排班表」。除了 cronPattern / timezone,它有一套额度控制:maxExecutions / remainingExecutions(null = 无限),外加 executionConditions JSONB 装 activeDays / activeHours / maxExecutionsPerDay(:10-14)。

agentSkills(agentSkill.ts:11)则是 agent 的可复用技能包:source 三档(builtin / market / user)、manifest 存版本作者、resources 是「虚拟路径 → 资源元信息」的映射、zipFileHash 按内容寻址指向原始分发包。

6.5 服务层:9 个服务各管一段

服务职责入口符号
task工单 CRUDTaskService(services/task/index.ts:104)
taskGraph把子任务按依赖切成可并行的层planSubtaskLayers(taskGraph/index.ts:71)
taskRunner跑一次工单TaskRunnerService.runTask(taskRunner/index.ts:68)
taskScheduler定时投递,本地 / QStash 两套实现createTaskSchedulerModule(taskScheduler/impls/index.ts:20)
taskLifecycle一次开工结束后的全部后续动作onTopicComplete(taskLifecycle/index.ts:140)
taskReview用 rubric 给产出打分TaskReviewService(taskReview/index.ts:36)
taskResultBridge把结果回传给「创建这个工单的那个 agent」TaskResultBridgeService(taskResultBridge/index.ts:100)
brief简报的读取与解决BriefService(brief/index.ts:33)
queue通用消息队列,同样本地 / QStash 双实现QueueService(queue/QueueService.ts:13)

taskGraphplanSubtaskLayers 用 Kahn 算法做拓扑分层,它对「拿不准的边」的处理很保守(taskGraph/index.ts:58-65):上游状态未知或不在本批里,就当外部阻塞,把依赖方排除并单独报告——绝不静默丢边,因为丢一条边就意味着一个任务在被阻塞时被放跑了。它还专门有一步找环(:190-193)。

6.6 走一遍:一次 heartbeat tick

QStash / setTimeout 到点


runHeartbeatTick(taskId, userId) heartbeatTick.ts:38
│ 直接读 tasks 行拿 workspaceId(系统级调度没有 workspace 上下文)
│ 逐项复查:not-found / terminal / mode-changed / no-interval
│ / in-flight / human-waiting → 任一命中就跳过

TaskRunnerService.runTask() taskRunner/index.ts:61
│ ① 没有 assignee → 回落 inbox agent
│ ② 已有 running 的 topic → 抛 CONFLICT
│ ③ 超过 heartbeatTimeout 的 topic → 标记超时
│ ④ buildTaskPrompt(带上上次的 handoff)
│ ⑤ 状态置 running,快照 model/provider 到 task.config

AiAgentService.execAgent() ── 第 1 章的 runtime 跑起来

▼(完成回调)
TaskLifecycleService.onTopicComplete() taskLifecycle/index.ts:108
│ updateHeartbeat → topic 状态 → 生成 handoff
│ → 合成 brief → 决定下一状态

automationMode 且到达 maxExecutions ─► completed
automationMode 未到额度 ─► scheduled(等下一 tick)
非自动化且该暂停 ─► paused(等人确认)

两处值得抄的工程细节:

  • 「DB 是权威」。两个 tick 函数的注释都写着同一句话(heartbeatTick.ts:33-35scheduleTick.ts:34-35):调度消息可能在用户暂停/取消/改模式之后才送达,所以每一项都要重新读库复查。这是所有延迟投递系统都得处理的问题。
  • weSetRunning 标志(taskRunner/index.ts:77-81)。catch 块里的回滚只在「是这次调用把它置成 running 的」时才执行,否则一次早期失败(比如因并发而抛的 CONFLICT)会把正在跑的那个 run 的状态踩成 paused

自动化任务成功时永远不会自动 pause,只有 reason === 'error' 才进 paused 等人来看(taskLifecycle/index.ts:238-241);出错时还会写一张 priority: 'urgent' 的 brief(:212-216)。


7. 信号总线:把 agent 的进展变成可订阅事件

7.1 三段式:source → signal → action

packages/agent-signal/ 的模型只有三个节点类型,含义分得很干净:

发生了什么 要不要管 去做什么
┌──────────┐ ┌──────────┐ ┌──────────┐
│ source │──────►│ signal │──────►│ action │
│ 客观事实 │ policy │ 值得关注 │handler│ 具体动作 │
└──────────┘ └──────────┘ └──────────┘
└──────── chain.rootSourceId 把整条链串起来 ────────┘

三个构造函数在 base/builders.ts:createSource(:70)、createSignal(:86)、createAction(:105)。它们做的事情高度一致——补 id、补时间戳、算 chain:

// 真实源码,builders.ts:52-67 节选
const buildChainRef = (rootSourceId, overrides, parentNodeId, parentSignalId, parentActionId) => ({
...overrides,
chainId: overrides?.chainId ?? `chain:${rootSourceId}`,
parentNodeId: overrides?.parentNodeId ?? parentNodeId,
rootSourceId,
});

每个下游节点都把上游的 rootSourceId 往下传,于是任何一个 action 都能一路溯源到最初那件事

7.2 scopeKey:事件归到哪条「车道」

AgentSignalScopeKey(source/scopeKey.ts:90)负责把一堆可选 id 压成一个确定的字符串。优先级是硬编码的(fromProducerInput,:98):

顺序有什么就用什么生成的 key
1topicIdtopic:<id>
2taskIdtask:<id>
3platform + applicationId + platformThreadIdbot:<platform>:<app>:<thread>
4agentId + userIdagent:<agentId>:user:<userId>
5userIduser:<id>
6都没有fallback:global

为什么需要它: 去重、限流、工作流交接都要一个稳定的分组键。有了它,「同一个 topic 里的事件」和「同一个 bot 会话里的事件」可以走各自的车道互不干扰。

createSourceEvent(source/sourceEvent.ts:55)就是入口:调用者给 sourceId(用于幂等)+ sourceType + payload,它自动补 scopeKeytimestamp。函数文档特意说明这样做的目的是让浏览器和服务端的生产者共用同一套事件形状,而不用去 import 服务端服务。

7.3 有哪些 source / signal

AGENT_SIGNAL_SOURCE_TYPES(source/sourceTypes.ts:5)列了 18 种源事件,分四类:

例子
agent 执行agent.execution.completed / .failedagent.user.message
自省触发agent.self_reflection.requestedagent.self_feedback_intent.declaredagent.nightly_review.requested
客户端网关client.gateway.stream_start / .step_complete / .runtime_end / .error
运行时与工具runtime.before_step / after_steptool.outcome.completed / .failed

而内置 signal 只有 3 种(types/events.ts:2):signal.action.applied / .skipped / .failed。这个不对称是有意的:source 是丰富的事实,signal 是策略产出的结论。策略层(SignalPlan,types/builtin.ts:7)带 policyId + scopeKey + signals[],谁产生的一目了然。

注意 agent.execution.completed 的 payload 有个坑,注释直接点了出来(sourceTypes.ts:43-47):非终态的暂停(waiting_for_async_tool / waiting_for_human)复用同一个 source,所以只关心「真的跑完了」的消费者必须自己过滤 reason

7.4 注册表:一种类型可以挂多个处理器

base/registries.ts 提供三个同构的注册表(source / signal / action)。内部实现只有 30 行(createRegistry,:54),关键在 register追加而不是覆盖(:71-80),match(type) 返回该类型的全部处理器。也就是说一个 source 可以同时喂给多条策略。

服务端的编排入口是 executeAgentSignalSourceEvent(apps/server/src/services/agentSignal/orchestrator.ts:222),它把 source event 依次穿过策略、runtime、receipt 持久化和 observability 投影。发射侧有三个层次(emitter.ts):emitAgentSignalSourceEvent(:110,直接发)、enqueueAgentSignalSourceEvent(:143,入队)、以及 emitAgentSignalSourceEventWithStore(orchestrator.ts:243)。

7.5 到前端:notification 与 push

信号变成用户可见的通知,在这个仓库里能看到的是下游的两段:

  • notifications(packages/database/src/schemas/notification.ts:9):category 做偏好开关的粗分组,type 是具体场景,dedupeKeyuserId 组成唯一索引实现幂等创建(:49)。索引全是带 where 的部分索引,分别服务收件箱列表、未读计数、归档清理三种查询。
  • PushChannel(apps/server/src/services/push/PushChannel.ts:30):基于 Expo Push。它做四步——查 token、Expo.isExpoPushToken 过滤畸形 token、分块并按 100ms 节流发送、把 (ticketId, expoToken) 对编码进 providerMessageId。之所以要编码,是为了让后面的对账 worker processPushReceipts(processPushReceipts.ts)能定位到失效 token 并删除;Expo 的回执只保留 24 小时,所以对账窗口是「发出后 15 分钟 ~ 24 小时」(:14-17)。

类注释明确说 PushChannel 是「结构上兼容 cloud 的 NotificationChannel,可以直接注册进 cloud 的 channel 表」(PushChannel.ts:20-22)。从 brief / signal 到 notification 的那段编排逻辑不在这个开源仓库里——代码里看不出来,这里不做推断。


8. 前端:把线性消息还原成对话树

8.1 问题是什么

数据库里的 messages扁平的,每行带一个 parentId。但用户看到的东西远比一条链复杂:一次编辑产生分支、一次广播产生并排的多个回复、一个工具调用下面挂着子 agent 的整条 thread、一次信号触发的回调要折叠起来。

packages/conversation-flow/ 就干这一件事:扁平数组进,两种表示出。

8.2 三相解析

messages[](扁平,带 parentId / threadId)

│ ① Indexing buildHelperMaps() indexing.ts:42
│ → messageMap / childrenMap / threadMap / messageGroupMap

│ ② Structuring buildIdTree() structuring.ts:11
│ → 只含主干的 id 树(带 threadId 的被过滤走)

│ ③ Transformation Transformer transformation/index.ts:16
│ → transformAll() 出 contextTree;flatten() 出 flatList

{ messageMap, contextTree, flatList }

入口是 parse()(parse.ts:23)。它在三相之前还有一步预处理:把 metadata.scope === 'sub_agent' 的消息的 agentId 换成 metadata.subAgentId(parse.ts:28-33)。注释说明了原因——不这么做,后面的分组逻辑会把不同 agent 的消息合并进同一组。

buildIdTree 只有 27 行,核心是那两句过滤:带 threadId 的消息不进主干树(structuring.ts:19-22:31-34)。thread 是侧枝,不是主干——这条规则让子 agent 的整条对话不会污染主对话的树形。

8.3 双表示:为什么要出两份

这是整个包最关键的设计决策:

┌─► ContextTree ── 树 ── 分支切换 / 语义理解 / 折叠块
messages[] ─parse─►┤
└─► FlatMessage[] ── 线性 ── 虚拟列表按行渲染
ContextTreeFlatMessage[]
形状嵌套节点树一维数组
包含所有分支(包括没激活的)只有激活路径
用途分支导航、语义操作、上下文理解虚拟滚动列表渲染
类型ContextNode 9 种(types/contextTree.ts:170)FlatMessageRole 12 种(types/flatMessageList.ts:31)
构建者ContextTreeBuilderFlatListBuilder

为什么不能只留一份?因为两边要的东西相反:树需要保留所有分支才能做「切到另一个分支」;虚拟列表需要一个稳定的一维索引才能算行高和滚动位置。硬要用一个,要么树里塞渲染细节,要么列表里塞递归。

FlatMessageRole 里有 5 个是 parse() 虚构出来的角色(flatMessageList.ts:20-25):assistantGroup(assistant + 它的工具调用聚合成一行)、messageGroupcompareagentCounciltasks。它们在数据库里不存在,只为渲染而生。另有 2 个是数据库聚合出来的:compressedGroup / compareGroup

8.4 ContextNode 的 9 种节点

节点什么时候出现定义
MessageNode普通一条消息contextTree.ts:30
AssistantGroupNodeassistant + 它的工具调用:39
CompareNode并排对比多个输出,只有一列进上下文:48
BranchNode同一父消息下的多条备选路径:61
AgentCouncilNode多 agent 并行回复,全部进上下文:75
TasksNode同一父下的多条 role='task' 消息:87
CompressedGroupNode被压缩的历史 + 保留的 pinned 消息:112
CompareGroupNode数据库侧聚合的并行模型回复:137
SignalCallbacksNode外部信号触发的反应式回合:155

CompareNodeAgentCouncilNode 的差别值得单独记:长得一样,语义相反。Compare 是「你挑一个」,所以有 activeColumnId,只有选中的那列进模型上下文;Council 是「大家都说了」,所有回复都进上下文(:73-74)——正好对应第 3 节的 broadcast,而 broadcast 的工具结果里带的 metadata: { agentCouncil: true }(builtin-tool-group-management/src/executor.ts:80)就是给这里认的标记。

SignalCallbacksNode 则是第 4.3 节写入规则的读取端:那些挂在 tool 消息下的、无工具的 assistant 回合,被收成一个折叠块,按 metadata.signal.sequence 排序,不折进主干的 assistant→tool→assistant 之字形(:143-153)。

8.5 五个协作者

Transformer(transformation/index.ts:16)本身只是个门面,真正干活的是五个类:

职责关键方法
MessageCollector收集相关消息:一个 assistant 的工具、一条 assistant 链collectToolMessages(MessageCollector.ts:72)、collectAssistantChain(:161)
MessageTransformer消息 → AssistantContentBlock;拆 usage/performancemessageToContentBlock(:17)、splitMetadata(:44)
BranchResolver决定哪个分支是激活的getActiveBranchId(BranchResolver.ts:28)
ContextTreeBuilder建树transformToLinear(:50)
FlatListBuilder建线性列表flatten(:39)

BranchResolver 的三级优先级很值得看(BranchResolver.ts:28-69):

  1. metadata.activeBranchIndex——若刚好等于 children.length,说明分支正在被创建(乐观更新),返回 undefined 而不是报错;
  2. 索引无效时,推断:哪个分支有子节点就选哪个;
  3. 都不行,选第一个。

splitMetadata(MessageTransformer.ts:45)则在兼容两代存储格式:嵌套的 metadata.usage = {...}(异构 agent / Gateway 写的)和扁平的 metadata.totalTokens(老写入路径)。嵌套优先,扁平补空缺。parse() 末尾还会把 metadata.usage 提升到顶层 usage 字段(parse.ts:139-141),注释解释得很实在:DB 把用量存在 metadata JSONB 里,服务端没有任何一处 transform 把它捞出来,索性在这个「构造展示形状」的地方一次性做掉,桌面端(本地 PGlite)和 Web(远程 Postgres)都受益。


9. 评测与可观测:闭环的最后一段

四个包组成运营侧的度量闭环:

干什么入口符号
eval-dataset-parser认格式 + 解析评测数据集detectFormat / parseDataset(src/index.ts:1-2)
eval-rubric按 rubric 给输出打分evaluate(src/evaluate.ts:45)
agent-tracing把每一步落成可回放的 ExecutionSnapshotappendStepToPartial / finalizeSnapshot(src/recorder/index.ts:8:32)
observability-otelOpenTelemetry 语义 span 与指标modules/agent-runtimemodules/agent-signal

evaluate 的流程(evaluate.ts:36-42):逐条 rubric 先抽取答案,若 expected 是 JSON 数组则做 any-of 匹配,再跑 matcher,最后加权求分,默认 0.6 阈值。

数据库侧有 7 张 agent_eval_* 表(schemas/agentEvals.ts):benchmarks / experiments / experimentBenchmarks / datasets / testCases / runs / runTopics。agentEvalRuns(:248)有 parentRunId 自引用,状态里除了常规几种还有一个 external(:272-274)——留给外部执行的评测。

agent-tracing 的 recorder 是增量追加的:每一步 appendStepToPartial 把 step 追进磁盘上的 partial,操作完成时 finalizeSnapshot 收成完整快照。这正是 agent_operations.traceS3Key 指向的那个东西。

observability-otel 的 agent-runtime 模块里有一个专门的指标值得一提:asyncToolResumeCounter(modules/agent-runtime/index.ts),它按 6 种 outcome 分类统计异步子 agent 的父级恢复尝试(resumed / barrier_held / no_pending / no_state / lost_cas / verify_exhausted)。注释写明了目的:让那些卡在 waiting_for_async_tool 的孤儿 operation 被看见,而不是默默堆积。这是第 3.6 节那套 deferred 机制的运维配套。


10. 巧妙之处(可以直接抄的)

  1. 同一个「大脑/引擎」形状复用两次。 单 agent 循环和群体编排循环共享 decide → instruction → execute → result 的骨架,只换语义(GroupOrchestrationSupervisor.ts:48 vs 第 1 章的 runner)。学一遍架构,读两个子系统。

  2. 让 agent 用「调工具」表达编排意图。 supervisor 不需要特殊协议,它调 speak 工具,工具返回 stop: true 并注册 afterCompletion 回调(builtin-tool-group-management/src/executor.ts:33-56)。编排能力因此天然复用了整套工具体系(第 4 章)。

  3. 纯 reducer + intent 列表 + commit-on-success。 异构 agent 的状态机不碰 I/O,返回 intent 让各环境自己执行;intent 失败则 state 不推进,重试时对着原 state 重放(mainAgentCoordinator/reducer.ts:14-19)。同一份逻辑同时服务渲染端和服务端持久化端。

  4. id 预分配消灭「先建后回填」。 reducer 提前 ctx.newId('message'),intent 里直接带上完整的 parentId 链(subagentCoordinator/types.ts:19-21)。

  5. 用 handoff 摘要代替上下文重放。 每次开工结束由 LLM 生成 { title, summary, keyFindings, nextAction }(task.ts:214-216),下次从摘要接上。这让一个跑几十轮的长任务不会线性膨胀。

  6. 一个布尔值决定指令类型,而不是决定执行分支。 runInClientexecute_task 变成 exec_client_async_task(GroupOrchestrationSupervisor.ts:111-116),执行面差异被挡在类型系统里。

  7. 拓扑分层拒绝静默丢边。 planSubtaskLayers 对状态未知的上游一律当外部阻塞并单独报告(taskGraph/index.ts:58-65)——宁可漏跑,不可错跑。

  8. 给异构 agent 生成 LLM 可读的岗位说明。 heteroAgentDescriptor.ts:43type: 'claude-code' 翻译成一段能力描述 + 能力标签,而不是让编排模型自己猜。

  9. DB 是唯一权威。 每个 tick 都重读任务状态,因为延迟消息可能晚于用户操作到达(heartbeatTick.ts:33-35)。

  10. 双表示分工。 树管语义与分支,一维数组管渲染性能;各自专注,谁都不用背对方的包袱(types/contextTree.ts vs types/flatMessageList.ts)。


11. 边界与局限(诚实清单)

  • maxRounds 在客户端硬编码 10(groupOrchestration.ts:21)。那段代码有一句「Get Group Configuration」的注释,但没有真的读群配置。
  • init 相位在生产路径上没人走。 全仓 type: 'init' 只在 GroupOrchestrationRuntime.ts:113 出现;客户端入口一律从 supervisor_decided 开始。
  • 客户端不用 runtime.run() store 手写循环以便逐步检查取消(groupOrchestration.ts:326-331),于是 run() 里的 abort 检查在生产上是死代码。
  • 编排工具只开放了一半。 delegate / interrupt / summarize / createWorkflow 在 manifest 里被注释掉(manifest.ts:68181198222),类型与 executor 已就位但模型看不见。
  • 四个内置 agent 目前是空转的。 nightly-review / self-reflection / self-feedback-intent 的工具面「在后续 PR 注册」,现在调用它们是设计上的 dormant(见各自 index.ts 的注释)。
  • verify 相关列正在迁移中。 agent_operations.verifyStatus / verifyPlan / verifyPlanConfirmedAt 已标 @deprecated,真值在 verify_runs(agentOperations.ts:80-88)。
  • tasks.status 的注释是旧的。 注释列 6 个状态,实际 7 个(多 scheduled)。
  • recentCwds 已废弃但列还在(device.ts:51)。
  • 从 brief/signal 到 notification 的编排不在本仓库。 PushChannel 只提供了「结构上兼容 cloud 的 channel」这一段(PushChannel.ts:20-22),中间的路由逻辑代码里看不出来。
  • GroupOrchestrationRuntime 对缺失 executor 直接抛错(:89),没有降级路径——executors 是 Partial<Record<...>>,注册不全会在运行时炸。

12. 代码地图(导航索引)

主题文件路径符号名
群体编排状态机packages/agent-runtime/src/groupOrchestration/GroupOrchestrationSupervisor.tsGroupOrchestrationSupervisor.decide
编排循环packages/agent-runtime/src/groupOrchestration/GroupOrchestrationRuntime.tsstep / run / createInitialState
编排指令与结果类型packages/agent-runtime/src/groupOrchestration/types.tsSupervisorInstruction / ExecutorResult
客户端编排驱动src/store/chat/slices/aiAgent/actions/groupOrchestration.tsinternal_execGroupOrchestration / triggerSpeak
客户端 executor 工厂src/store/chat/agents/GroupOrchestration/createGroupOrchestrationExecutors.tscreateGroupOrchestrationExecutors
服务端编排(deferred 版)apps/server/src/services/toolExecution/serverRuntimes/groupManagement.tsGroupManagementExecutionRuntime
编排工具执行器packages/builtin-tool-group-management/src/executor.tsspeak / broadcast / executeAgentTask
子 agent 指令packages/agent-runtime/src/types/instruction.tsSubAgentTask / SubAgentResultPayload / AgentInstructionExecSubAgent
子 agent 相位路由packages/agent-runtime/src/agents/GeneralChatAgent.tstool_result / sub_agent_result 分支
异构主 agent reducerpackages/heterogeneous-agents/src/mainAgentCoordinator/reducer.tsreduce(导出为 reduceMainAgent)/ computeTurnParentId
异构子 agent reducerpackages/heterogeneous-agents/src/subagentCoordinator/reducer.tsreduce(导出为 reduceSubagentRuns)/ finalizeRun
事件域判定packages/heterogeneous-agents/src/subagentCoordinator/getEventScope.tsgetEventScope
agent HR 系统packages/agent-manager-runtime/src/AgentManagerRuntime.tsAgentManagerRuntime
异构 agent 描述器packages/agent-manager-runtime/src/heteroAgentDescriptor.tsdescribeHeterogeneousAgent / HETERO_PROFILES
内置 agent 花名册packages/builtin-agents/src/index.tsBUILTIN_AGENTS / BUILTIN_AGENT_SLUGS
群主管人设packages/builtin-agents/src/agents/group-supervisor/index.tsGROUP_SUPERVISOR
agent 文档模板packages/agent-templates/src/template.tsDocumentTemplate / DocumentTemplateManager
operation 表packages/database/src/schemas/agentOperations.tsagentOperations
工单及卫星表packages/database/src/schemas/task.tstasks / taskDependencies / taskTopics / briefs / taskComments
排班表packages/database/src/schemas/agentCronJob.tsagentCronJobs
技能表packages/database/src/schemas/agentSkill.tsagentSkills
群与成员packages/database/src/schemas/chatGroup.tschatGroups / chatGroupsAgents
设备表packages/database/src/schemas/device.tsdevices
评测表packages/database/src/schemas/agentEvals.tsagentEvalRuns / agentEvalDatasets
工单执行apps/server/src/services/taskRunner/index.tsTaskRunnerService.runTask
心跳 / 定时 tickapps/server/src/services/taskRunner/heartbeatTick.tsscheduleTick.tsrunHeartbeatTick / runScheduleTick
工单生命周期apps/server/src/services/taskLifecycle/index.tsTaskLifecycleService.onTopicComplete
依赖拓扑分层apps/server/src/services/taskGraph/index.tsplanSubtaskLayers / TaskGraphService
调度器双实现apps/server/src/services/taskScheduler/impls/LocalTaskScheduler / QStashTaskScheduler
结果回传创建者apps/server/src/services/taskResultBridge/index.tsTaskResultBridgeService
简报服务apps/server/src/services/brief/index.tsBriefService
信号节点构造packages/agent-signal/src/base/builders.tscreateSource / createSignal / createAction
信号注册表packages/agent-signal/src/base/registries.tscreateSourceProcessorRegistry
作用域键packages/agent-signal/src/source/scopeKey.tsAgentSignalScopeKey / getSourceEventScopeKey
源事件构造packages/agent-signal/src/source/sourceEvent.tscreateSourceEvent
源/信号类型表packages/agent-signal/src/source/sourceTypes.tstypes/events.tsAGENT_SIGNAL_SOURCE_TYPES / AGENT_SIGNAL_TYPES
服务端信号编排apps/server/src/services/agentSignal/orchestrator.tsexecuteAgentSignalSourceEvent
推送通道apps/server/src/services/push/PushChannel.tsPushChannel
对话解析入口packages/conversation-flow/src/parse.tsparse
三相:索引/建树packages/conversation-flow/src/indexing.tsstructuring.tsbuildHelperMaps / buildIdTree
三相:变换协调packages/conversation-flow/src/transformation/index.tsTransformer
树构建packages/conversation-flow/src/transformation/ContextTreeBuilder.tsContextTreeBuilder.transformToLinear
列表构建packages/conversation-flow/src/transformation/FlatListBuilder.tsFlatListBuilder.flatten
分支选择packages/conversation-flow/src/transformation/BranchResolver.tsBranchResolver.getActiveBranchId
节点类型定义packages/conversation-flow/src/types/contextTree.tsflatMessageList.tsContextNode / FlatMessageRole
评分packages/eval-rubric/src/evaluate.tsevaluate
轨迹录制packages/agent-tracing/src/recorder/index.tsappendStepToPartial / finalizeSnapshot

13. 接着读什么

  • 01-agent-runtime-kernel.md —— 本章的「大脑/引擎」骨架第一次出现的地方,先读它再回来看第 3 节最省力。
  • 02-context-engine.md —— agent-templates 的 loadPosition / policyLoad 最终由它消费。
  • 04-tools-and-plugins.md —— stop: truedeferred: trueregisterAfterCompletion 这些工具协议字段的完整语义。
  • 05-execution-planes.md —— 本章反复出现的「同一条指令,客户端 / 服务端两套驱动」的系统性解释。