跳到主要内容

Coordinator 模式:从目标到任务 DAG

30 秒导读: open-multi-agent 的招牌能力是运行时动态编排——你给一句大白话目标,框架临时拉起一个 "coordinator" 智能体,让它把目标当场拆成一组 JSON 任务(带依赖),再把这组任务编成一张有向无环图(DAG)按依赖并行跑,最后由 coordinator 把各任务结果合成成最终答案。本章讲透这条主线:orchestrator.ts:runTeam 的每一步,以及给它派活的 scheduler.ts 四种策略。

本章面向已经知道"多智能体框架大概是什么"的读者(不了解请先看 index)。读完你应该能讲清楚:一句目标是怎样变成一张任务图、图里的任务怎样被分派给智能体、以及这套"临时算出来的编排"相比 LangGraph/Mastra 那种"预先画死的图"取舍在哪。

本章不展开两件事:任务队列内部的状态机(pending→in_progress→completed/failed/blocked 的转移、失败级联、重试怎么落地)留给 02-task-queue-and-execution;单个 Agent 的对话循环与工具执行留给 03-agent-loop-and-tools。这里只讲"编排层"怎么把目标变成图、怎么调度这张图。


1. 先建直觉:什么叫"运行时动态编排"

1.1 静态图 vs 动态编排

大多数 agent 编排框架要你先画好一张图:哪些节点、谁连谁、什么条件走哪条边,都在代码里写死,运行时只是"沿着图走"。

open-multi-agent 反过来:图是运行时由一个 LLM 现算的。你只给两样东西——

  • 一句目标(goal),例如 "写一份 TypeScript 5.5 的报告";
  • 一支智能体名单(roster),例如 researcher / writer

框架把目标 + 名单丢给一个临时的 coordinator 智能体,让它回一段 JSON:"要做这几件事、每件谁做、谁依赖谁"。这段 JSON 就是这一次运行的编排图。换个目标,图就完全不同——没有一行代码预先规定过任务长什么样

1.2 一句类比

把 coordinator 当项目经理:你(老板)扔来一句需求,PM 当场把它拆成工单派给组员,组员并行干活、有先后依赖的排队等,最后 PM 把大家的产出汇总成一份交付物。区别只是这里的 PM 和组员都是 LLM。

1.3 最小使用样子

runTeam 就是"把目标交给这个 PM"的入口(orchestrator.ts:27 文档示例):

// 示意,基于 orchestrator.ts 顶部的 quick-start 示例
const orchestrator = new OpenMultiAgent({ defaultModel: 'claude-opus-4-6' })

const team = orchestrator.createTeam('research', {
name: 'research',
agents: [
{ name: 'researcher', systemPrompt: 'You are a researcher.' },
{ name: 'writer', systemPrompt: 'You are a technical writer.' },
],
sharedMemory: true,
})

// 一句目标进去,一份合成结果出来
const result = await orchestrator.runTeam(team, 'Produce a report on TypeScript 5.5.')
console.log(result.agentResults.get('coordinator')?.output) // 最终合成答案

你不需要告诉它 "先让 researcher 查、再让 writer 写"——这个先后顺序是 coordinator 自己拆出来的


2. 顶层全景:runTeam 一次跑完的五步

runTeam 的真身在 orchestrator.ts:1684 async runTeam(team, goal, options)。撇开分支,主干是五步,外加两道审批门和一层成本闸

先看这张流程图(从上到下是时间顺序,虚线是可选的门/闸):

一句 goal + 一支 roster

┌────────────▼─────────────┐
│ ① 短路判定 isSimpleGoal │ 简单目标?→ 跳过 coordinator,
│ selectBestAgent │ 直接派给最匹配的单个 agent,返回
└────────────┬─────────────┘
│ 复杂目标
┌────────────▼─────────────┐
│ ② 拆解:coordinator.run │ buildDecompositionPrompt → LLM
│ 吐出 JSON 任务数组 │ parseTaskSpecs 解析
└────────────┬─────────────┘

┌────────────▼─────────────┐
│ ③ 入队:loadSpecsIntoQueue│ title→taskId 映射依赖,
│ 建成任务 DAG │ 建 TaskQueue
└────────────┬─────────────┘

┌────────────▼─────────────┐
│ ④ 派活:scheduler.autoAssign 给没 assignee 的任务分派 agent
│ · · · onPlanReady 审批门 · · · │ ← 可拒:拒了直接返回 success:false
│ · · · planOnly 只出计划 · · · │ ← 只要计划不执行
│ executeQueue 轮次并行分发 │
│ · · · onApproval 逐轮审批门 · · │ ← 每轮之间可叫停
└────────────┬─────────────┘

┌────────────▼─────────────┐
│ ⑤ 合成:runCoordinatorSynthesis │ 把各任务结果喂回 coordinator,
│ 产出最终答案 │ 写进 result.agentResults['coordinator']
└────────────┬─────────────┘

TeamRunResult

贯穿全程:token budget 检查(每步之后累加,超了就停并 skipRemaining)

各步骤职责一句话(文件都在 packages/core/src/orchestrator/orchestrator.ts):

步骤干什么关键符号
① 短路目标简单就不劳驾 coordinator,直接单 agent 跑isSimpleGoal · selectBestAgent(orchestrator.ts:165 / :188)
② 拆解让 coordinator 把目标吐成 JSON 任务数组buildDecompositionPrompt · parseTaskSpecs(:2472 / :464)
③ 入队把 title 依赖映射成 taskId,建成 DAGloadSpecsIntoQueue(:2819)
④ 派活+执行分派 agent、轮次并行分发Scheduler.autoAssign · executeQueue(scheduler.ts:156 / orchestrator.ts:798)
⑤ 合成汇总各任务结果成最终答案runCoordinatorSynthesis(:2531)

下面逐步拆开。


3. 第一步:短路——简单目标不必开会

3.1 它要解决的小问题

不是每个目标都值得开一场"拆解 + 派活 + 合成"的会。"把这段话翻译成中文"这种单动作目标,拉起 coordinator 反而多烧两趟 LLM(拆解一趟、合成一趟)、更慢更贵。所以 runTeam 开头先问一句:这目标简单吗?

3.2 怎么判"简单"

isSimpleGoal(goal)(orchestrator.ts:165)的规则很保守——同时满足两点才算简单:

  1. 长度 ≤ SIMPLE_GOAL_MAX_LENGTH(200 字符,orchestrator.ts:149);
  2. 不命中任何一条 COMPLEXITY_PATTERNS(orchestrator.ts:114)。

COMPLEXITY_PATTERNS 是一组正则,每条盯住一类"这需要多智能体协作"的信号:

信号类别命中的样子例子正则(节选)
显式排序"first … then"、"step 1"、编号列表/\bfirst\b.{3,60}\bthen\b/i · /^\s*\d+[\.\)]/m
协作指令"collaborate with X"、"coordinate the team"/\bcollaborat(?:e|ing)\b\s+(?:with|on|to)\b/i
并行执行"in parallel"、"concurrently"/\bin\s+parallel\b/i
多产出连接"build X … and deploy Y … test Z"/\b(?:build|create|…)\b.{5,80}\b(?:and|then)\b.{5,80}\b(?:…|deploy)\b/i

一个关键的克制: 协作类正则只在祈使指令下命中(collaborate withcoordinate the team),而描述性用法——"how do pods coordinate state"、"explain microservice collaboration"——不算复杂(orchestrator.ts:122-130 注释明说这条设计意图)。这样避免了"用户只是在问问题里带了 coordinate 一词就被误判成要开会"。

3.3 简单就单点派活

一旦判定简单,runTeam 走短路分支(orchestrator.ts:1702-1776):用 selectBestAgent(goal, agents)(:188)挑一个最匹配的 agent,直接 agent.run(goal),不进队列、不合成,把结果包成一个 id: 'short-circuit' 的伪任务记录返回。

selectBestAgent 的打分刻意和 scheduler 的 capability-match 策略一模一样(见 §7.3),两边共用 extractKeywords / keywordScore(utils/keywords.ts),保证"短路选谁"和"正常派活选谁"口径一致(orchestrator.ts:170-212 注释强调这份镜像)。

注意一个例外: planOnly 模式不走短路——if (!options?.planOnly && … && isSimpleGoal(goal))(orchestrator.ts:1702)。因为 planOnly 的用途是"只要一张计划",而短路根本不产生计划,所以强制走 coordinator 拆解那条路。


4. 第二步:拆解——让 coordinator 吐 JSON

4.1 给 coordinator 灌什么

coordinator 不是团队里的某个 agent,而是 runTeam 临时造的一个 name: 'coordinator' 智能体(buildCoordinatorBaseConfig,orchestrator.ts:2489,maxTurns 默认 3)。它收到两段拼好的 prompt:

  • system prompt(buildCoordinatorSystemPrompt,:2373)= 角色说明 + 团队名册 + 输出格式 + 合成指引;
  • user prompt(buildDecompositionPrompt,:2472)= "把下面这个目标拆成给 (names) 的任务,只回 JSON 数组"。

输出格式是这段的灵魂(buildCoordinatorOutputFormatSection,:2429)。它硬性要求 coordinator 只回一个 JSON 数组,每个任务对象含四个字段:

字段含义
title短标题(后面用来被别的任务引用)
description完整描述,含上下文和期望产出
assignee名册里某个 agent 名字
dependsOn依赖的任务标题数组(可空)

注意 dependsOn 里放的是别的任务的 title,不是 ID——因为此刻任务还没入队、还没 ID,coordinator 只能靠标题互相引用(§5 讲这层映射)。

prompt 里还有一段**"依赖最小化"指引**(:2449-2454):让 coordinator 优先以 assignee 的 system prompt 为信号判断它到底消费哪些上游产出,不确定就少连依赖——因为多一个父任务就少一分并行、多一分 token。这是把"图要瘦"这个工程直觉直接写进了 prompt。

4.2 跑一趟、解析回来

coordinatorAgent.run(decompositionPrompt)(orchestrator.ts:1804)拿到原始文本后,交给 parseTaskSpecs(raw)(:464)解析。这个解析器对 LLM 输出的脏很宽容:

  1. 先找 ```json … ``` 代码围栏,找不到就拿整段;
  2. 再在里面找第一个 [ 和最后一个 ],截出中间那段当 JSON;
  3. JSON.parse 后逐项校验:titledescription 必须是 string,否则跳过该项;其余字段(assignee/dependsOn/priority/verify…)缺了就置 undefined

解析产出 ParsedTaskSpec[](:393)。解析失败(返回 null)不致命:runTeam 退化成"每个 agent 分一个以 goal 为描述的任务"(orchestrator.ts:1839-1850)——即使 coordinator 吐了废话,团队也不会空转。

成本闸在这里第一次落下: 拆解跑完立刻累加 token,若已超 maxTokenBudget,直接返回、不再往下(orchestrator.ts:1810-1824)。


5. 第三步:入队——把标题依赖映射成任务图

这是"JSON 数组 → 真正的 DAG"的关键一跳,发生在 loadSpecsIntoQueue(orchestrator.ts:2819)。

5.1 为什么要两遍扫描

coordinator 用标题表达依赖("Write report" 依赖 "Research topic"),但队列里任务靠 ID 连边。ID 是 createTask 生成的,得先建出来才有。所以是两遍扫描:

第一遍:为每个 spec 建 task(先不连依赖)→ 得到稳定的 taskId
同时建 title→taskId 映射表 titleToId
第二遍:遍历每个 spec 的 dependsOn(一堆 title)
用 titleToId 把每个 title 翻译成 taskId → 连成 dependsOn 边 → 入队

5.2 标题歧义与悬空依赖怎么处理

真实 LLM 输出会有坑,loadSpecsIntoQueue 各堵了一个:

  • 重复标题:先数一遍每个标题出现几次(titleCounts,:2834);只有唯一的标题才进 titleToId(:2860)。若某依赖指向一个出现多次的标题,它无法被解析——归入 unresolvedDeps 并标注 (ambiguous duplicate title)(:2887-2888)。
  • 解析既认 ID 也认 title:dependsOn 里的每个引用先按 ID 找、再按标题找(byId?.id ?? byTitle,:2880-2883),两头都不中就是悬空。
  • 悬空依赖不静默:带悬空引用的任务照样入队,但立刻 queue.fail 并写明是哪几个引用没解析(:2897-2902)——让它显式失败,而不是永远卡 blocked。

标题匹配用的是 normalizeTitle(小写 + trim,:2833),对大小写和首尾空格不敏感。

至此,TaskQueue 里就是一张带依赖边的任务 DAG。这张图的入队/状态机细节(依赖满足了怎么自动解锁、失败怎么级联到下游)是下一章 02-task-queue-and-execution 的主题,本章不展开。


6. 第四步:派活 + 轮次并行执行

6.1 先派活:autoAssign

coordinator 可能给某些任务留了 assignee,也可能没给。Scheduler.autoAssign(queue, agents)(scheduler.ts:156)负责给所有 pending 且没 assignee 的任务补上 agentrunTeam 默认用 new Scheduler('dependency-first')(orchestrator.ts:1832)。四种策略是本章后半的重点(§7)。

关键设计:autoAssign 不是只跑一次executeQueue 的每一轮开头都重跑一次(orchestrator.ts:824),因为上一轮完成后可能有任务从 blocked 解锁成 pending、这些新 pending 还没 assignee,得在分发前补派。

6.2 executeQueue:while 轮次并行分发

执行主循环 executeQueue(orchestrator.ts:798)按**轮(round)**推进,不是一个个跑,而是"一批一批并行":

while (true):
├─ abortSignal 中止?→ skipRemaining,退出
├─ scheduler.autoAssign(...) 给新解锁的任务补派
├─ pending = queue.getByStatus('pending')
├─ pending 为空?→ 退出(要么全完成,要么剩下的都 blocked/failed 卡死)
├─ 把这批 pending 全部并行 dispatch(pending.map(async ...))
│ 每个任务:标 in_progress → 建 prompt → executeWithRetry(pool.run)
│ → 成功 queue.complete / 失败 queue.fail
├─ await Promise.all(dispatchPromises) 等整批结束
├─ token budget 超了?→ skipRemaining,退出
└─ onApproval 门:批完之后、下一轮之前,可叫停

一句话:每轮把"当前所有依赖已满足的任务"一次性并行跑掉,跑完再看解锁了谁,进下一轮。独立任务天然同轮并行(受 AgentPoolmaxConcurrency 限流,默认 5,orchestrator.ts:97);有依赖的任务自然排到后面的轮次。

每个任务的 prompt 由 buildTaskPrompt(:1118)拼:任务标题 + 描述 + 默认只注入直接依赖任务的产出(default-deny,:1149-1161;memoryScope: 'all' 才给全量共享记忆)。这套上下文注入与共享记忆的细节属于 05-memory-consensus-observability。

6.3 两道审批门 + 一层成本闸

runTeam 在执行前后埋了三个可选的控制点,让调用方能"看一眼再放行":

控制点时机触发后行为位置
onPlanReady(planTasks)DAG 建好、执行返回 false → 整个 run 直接返回 success:false,一个任务都不跑orchestrator.ts:1897-1919
planOnly: true同上,执行前把计划打包成 TaskExecutionRecord[] 返回,planOnly: true,不执行:1921-1945
onApproval(done, next)每一轮之间返回 false 或抛错 → skipRemaining,停止后续轮次executeQueue :1083-1101
token budget每一步之后累计 token > maxTokenBudget → 置 budgetExceededTriggered,skipRemaining 并退出拆解 :1810 · 执行 :1006-1020 · 合成 :2563

onPlanReady 是"人在环"审批门:先看 coordinator 拆出来的计划、觉得不对就一票否决。planOnly 更进一步——它配合 createPlanArtifact(:1999)把计划固化成 PlanArtifact,之后可以用 runTasks 重放同一张图(拆解一次,执行多次)。onApproval 则是逐轮闸门,适合"每完成一批让我确认再往下"的场景。

token budget 是贯穿全程的成本熔断:coordinator 拆解、每个 worker 任务、合成三处都在结果回来后累加并比对预算,一旦越线就 skipRemaining 把剩余任务标记跳过、优雅收场,而不是硬跑到没钱。


7. 调度策略:scheduler.ts 的四种派活法

Scheduler(scheduler.ts:96)只干一件事:把"没人认领的 pending 任务"映射到"某个 agent 名字"。它在调用之间无状态(唯一的可变状态是 round-robin 的游标),所有任务状态都存在 TaskQueue 里。核心方法 schedule(tasks, agents)(:126)先过滤出 pending && !assignee 的任务,再按策略分派。

四种策略如下:

策略核心逻辑何时合适位置
round-robin按游标轮流发给每个 agent,游标跨调用累进任务同质、只想均摊:180
least-busy发给当前 in_progress 任务数最少的 agent;平批内模拟累加负载避免扎堆任务耗时不均、想动态均衡:200
capability-match按关键词重叠给 agent 打分,最高分中标agent 各有专长、想按能力对口:245
dependency-first(默认)按"卡住的下游任务数"排序,关键路径任务优先拿 agent复杂多步流水线:294

7.1 round-robin:最简单的均摊

scheduleRoundRobin(:180)拿一个 roundRobinCursor,每派一个任务游标 +1、对 agent 数取模。游标跨调用保留,所以连续多轮调用会接着上次往下发,而不是每轮都从 agent[0] 重新开始(:97-98 注释点明这个意图)。

7.2 least-busy:看谁闲

scheduleLeastBusy(:200)先扫全部任务,数出每个 agent 手上有几个 in_progress(:207-212),然后每个待派任务都挑当前负载最低的 agent。同一批里派完一个就把该 agent 的模拟负载 +1(:232),避免一批任务全砸给同一个"当前最闲"的 agent。平手时按 agent 在数组里的顺序(靠前优先)。

7.3 capability-match:按能力对口

scheduleCapabilityMatch(:245)是关键词亲和度打分。它给每个 (任务, agent) 对双向打分再相加:

  • scoreA = 用任务关键词去数 agent 文本(name + systemPrompt)命中几个;
  • scoreB = 用 agent 关键词(name + systemPrompt + model)去数任务文本命中几个;
  • score = scoreA + scoreB,最高分中标(:266-278)。

一个不对称细节: agent 关键词那侧算进了 model(:255),文本那侧没有(:269)。这样"目标里提到模型名(如 haiku)"能加分到绑定该模型的 agent,但反向匹配不会被模型名污染——selectBestAgent(§3.3)用的是同一套逻辑和同一句注释(orchestrator.ts:196-203)。

打分底层是 extractKeywords / keywordScore(utils/keywords.ts):分词后丢掉长度 ≤3 的词和停用词,每个关键词命中最多计 1 分。是纯词面重叠,不是语义相似——够轻量,也够粗。

7.4 dependency-first:关键路径优先(默认)

这是 runTeam 的默认策略,也是最有意思的一个。它的假设是:能解锁最多下游任务的任务最该先拿到 agent——因为它卡在关键路径上,早点做完,后面一大片才能动起来。

criticality 怎么算: countBlockedDependents(taskId, allTasks)(scheduler.ts:51)是一趟前向 BFS,数出"有多少任务(传递地)在等这个任务完成":

1. 建反向邻接表 dependents: 依赖ID → [依赖它的任务们]
2. 从 taskId 出发做 BFS,每碰到一个依赖它的任务就加入 visited、继续往下
3. 不重复访问、种子任务本身不计
4. 返回 visited.size = 传递闭包里的下游任务总数

scheduleDependencyFirst(:294)按这个数降序排任务(下游越多越靠前,:301-305),然后在同批里用 round-robin 游标依次发给 agent——高关键度任务先挑,但仍轮流铺开、不把活全压给一个 agent。

举个例子帮直觉。设依赖关系是:

A ──► C ──► D
B ──► C
C ──► E
  • countBlockedDependents(A) = {C, D, E} = 3
  • countBlockedDependents(C) = {D, E} = 2
  • countBlockedDependents(D) = 0,E = 0

于是 A 排最前——先做 A(和 B)最划算,因为它们卡着 C 这条主干,C 一通,D/E 才有戏。


8. 第五步:合成——把结果收成一个答案

所有任务跑完后,runCoordinatorSynthesis(orchestrator.ts:2531)让 coordinator 再跑一趟,把散落的任务产出汇成最终答案。

buildSynthesisPrompt(:2588)把队列里的任务按状态分区拼进 prompt:

  • completed 任务:### 标题 (completed by 谁)\n产出;
  • failed 任务:单列一段 ## Failed Tasks,写明错误;
  • skipped 任务:单列 ## Skipped Tasks,写明原因(如"approval rejected");
  • 若开了共享记忆,附上 sharedMem.getSummary()

最后一句指令让 coordinator "把上面结果综合成一份覆盖原目标的完整答案,若有任务失败/跳过要点出结果里的空缺"(:2627-2629)。这个诚实收尾——把失败和跳过明确摊给合成者——保证部分失败时最终答案不会假装一切正常。

合成前有两道短路:abortSignal 已中止、或已超预算,直接返回 null,runTeam 就返回原始任务产出、不合成(:2544-2550 / :1974-1977)。合成结果最终以 'coordinator' 为 key 写进 result.agentResults(:1978),就是你 §1.3 里 result.agentResults.get('coordinator')?.output 拿到的那份。


9. 横向对比:动态编排 vs 静态图(LangGraph / Mastra)

同样是"多步骤 agent 编排",取舍很不一样:

维度open-multi-agent(动态编排)LangGraph / Mastra(静态图)
图从哪来运行时 coordinator 现算的 JSON 任务数组编码期开发者预先画死的节点 + 边
改需求换一句 goal,图就变,无需改代码通常要改图定义、重新部署
可预测性弱——图由 LLM 决定,每次可能不同强——图固定,行为可复现、可测
调试难——得看 coordinator 拆出了什么易——图是显式的、可视化的
适合场景目标开放、结构因输入而异的任务流程稳定、要审计/可复现的生产流水线

open-multi-agent 也没把自己锁死在"每次都现算":planOnly + createPlanArtifact + runTasks(见 §6.3)让你把一次拆解的结果固化成 PlanArtifact 反复重放——相当于"用 LLM 生成一次图,之后当静态图用"。这是它给"动态灵活"和"静态可复现"之间搭的桥。

需要对比这条动态编排主线时,可对照本 shelf 里采用静态图的兄弟项目;跨库的编排范式对照见总库 doc。


10. 边界与局限(本章范围内)

  • 编排质量 = coordinator 质量。 整张 DAG 由一次 LLM 调用决定;coordinator 拆错(漏任务、乱连依赖、指错 assignee),下游没有自动纠错——顶多靠 onPlanReady 人工拦一道。
  • 调度是词面匹配,不是语义。 capability-match / selectBestAgent 靠关键词重叠,同义词、隐含能力它读不出来(utils/keywords.ts)。
  • criticality 只看结构不看工时。 dependency-first 按"下游任务数"排序,不估每个任务实际多耗时,可能把一个下游多但很快的任务排在一个下游少但极慢的任务前面。
  • 单次拆解、不迭代。 runTeam 是"拆一次 → 执行 → 合成一次",执行中途不会回头让 coordinator 重新规划(README 明确写 "a single coordinator pass")。
  • 重复标题会丢依赖。 两个任务同标题时,指向它的依赖无法解析、直接判失败(§5.2)——coordinator 起标题得保证唯一。

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

全部在 packages/core/src/orchestrator/ 下。用符号名 grep 比行号抗漂移。

主题文件符号
runTeam 主流程(五步)orchestrator/orchestrator.tsOpenMultiAgent.runTeam
短路判定orchestrator/orchestrator.tsisSimpleGoal · COMPLEXITY_PATTERNS · SIMPLE_GOAL_MAX_LENGTH
短路选 agentorchestrator/orchestrator.tsselectBestAgent
coordinator prompt 组装orchestrator/orchestrator.tsbuildCoordinatorSystemPrompt · buildCoordinatorOutputFormatSection · buildDecompositionPrompt
coordinator 配置orchestrator/orchestrator.tsbuildCoordinatorBaseConfig
解析任务 JSONorchestrator/orchestrator.tsparseTaskSpecs · ParsedTaskSpec
title→taskId 映射建图orchestrator/orchestrator.tsloadSpecsIntoQueue
轮次并行执行orchestrator/orchestrator.tsexecuteQueue
任务 prompt 拼装orchestrator/orchestrator.tsbuildTaskPrompt
合成orchestrator/orchestrator.tsrunCoordinatorSynthesis · buildSynthesisPrompt
计划固化/重放orchestrator/orchestrator.tscreatePlanArtifact · PlanArtifact
调度器四策略orchestrator/scheduler.tsScheduler.schedule · autoAssign
round-robin / least-busyorchestrator/scheduler.tsscheduleRoundRobin · scheduleLeastBusy
capability-matchorchestrator/scheduler.tsscheduleCapabilityMatch
dependency-first + 关键度orchestrator/scheduler.tsscheduleDependencyFirst · countBlockedDependents
关键词打分utils/keywords.tsextractKeywords · keywordScore

接着读: 任务图入队后的状态机、依赖解锁、失败级联、重试 → 02-task-queue-and-execution;被派到的单个 agent 内部怎么跑 → 03-agent-loop-and-tools