refactor(workflow): DAG 编排剥成 @helios/workflow,依赖只有 ports(A-9 部分) - #56
Merged
Conversation
added 2 commits
August 31, 2026 20:58
## A-9 的前提只对了一半 原表述是「DAG 编排比 session 高一层,kernel 的职责是跑一个 run」。查下来不成立: workflow 不是 kernel 的上层,它是 kernel **注册给模型的一个工具** (`agentSpec/agentTools.ts`),运行时嵌在 session 里、靠会话级 `DerivedAgentExecutor` 干活。 而且照字面搬会**包级循环依赖**:workflow.ts 的唯一调用方在 kernel 内,它自己又反过来 import kernel 的 load / validate / selectTools / portResolver / derivedAgentExecutor。 所以这期只剥纯算法层。把它抬到 kernel 之上(做成 extension)**依赖 A-8**: `agentTools.ts` 现在靠 `executorFor: (sessionId) => …` 取会话级执行器, 那正是 A-8 要删的 service locator。 ## 做了什么 新包 `@helios/workflow`,**依赖只有 `@helios/ports`**,方向 kernel → workflow 单向: - `compile.ts` 纯图算法(解引用、查环、拓扑分层、定唯一终止节点) - `schedule.ts` 只按计划调度,对"派生执行器"的需求以**窄接口**表达 (`WorkflowControl` / `NodeOutcome` / `WorkflowRunContext`),不 import kernel 的类 三个共用纯函数落到 `ports/agentSpec.ts`(两个包都要用,公共部分只能放共同下层, 与 `skill.ts` 的 `parseFrontmatter` 同一先例):`isWorkflowSpec` / `resolveDefinition` / `workflowNodeId`。**带前缀是刻意的**——`nodeId` 在 `session.ts` 里指消息树节点,同名不同义。 顺带收紧两处: - `CompileContext` 的 `allTools` + `portResolver` 两个字段收成一个 `checkAssembly` (「这个 agent 装得起来吗」是 kernel 的知识,workflow 只要「是/否 + 原因」) - `WorkflowRunContext.executor` 是**死字段**,`runWorkflow` 从来没用过它,删掉 ## 护栏 `packages/workflow/test/dependency-direction.test.ts`:对源码 import 与 package.json 的静态断言,含一条「护栏本身不是空的」自检。⚠️ 这类反向依赖 **typecheck 抓不到**:pnpm workspace 里 `@helios/kernel` 本来就解析得到, TS 愉快编过,只在拆包/发版/拓扑构建时才炸。 变异 9/9 命中(源码 import kernel / package.json 加 kernel 依赖 / 护栏扫错目录 / 分层按完成顺序 / 多终止节点放过 / 上游拼接乱序 / 节点失败仍跑下游 / checkAssembly 不调 / 嵌套深度上限失效)。 ## 顺带修掉三处测试质量问题 **① workflow live 用例长期假红。** 它把代码属性和模型能力混在一条断言里。补两条确定性断言 (joiner 的 prompt 里确实有 `<upstream node="pw">` 块 —— 那正是本次搬走的 `buildNodeInput`), 再按**语义判据**分流:上游自己都没拿到口令 = 叶子没干活(模型问题)→ skip; 上游有、终点没有 = 数据没流过来 → 硬红。 不按标记形态识别是吃过教训的:实测已见四种形态(`<|DSML|invoke>` / `<use_mcp_tool>` / `<function_calls>` / `<tool_use>`),我追加过一轮又漏了第四种——无穷枚举。 **② eval 的分母一直是错的。** 以前一律拿全集 `DEFECTS.length` 作分母,于是每轮加缺陷都会 把所有场景的分数稀释一遍。我曾把 `39/41 → 41/44 → 24/46` 当趋势读,那是三个不同的量。 现在各场景按自己的文件范围算分母(分子也过滤,否则会打出 `10/8` 这种数字)。 而且 readonly 广度扫描已经**撞了 600s 上限被截断**(单跑必超)——随 eval set 增长必然再撞。 范围钉死到 4 个文件,它只负责回答「一次广度扫描能不能捞到有意义的东西」。 **③ CLI e2e 两个用例间歇假红。** `runCliArgs` 完全没有内部看门狗,子进程不退出就靠 vitest 的 5s 兜底——而那条路径**不杀子进程**,留下 tsx 孤儿,报错还只有「Test timed out」。 `configDiscovery` 更荒谬:内部看门狗 10s > 外层 5s,那句带 CLI 输出的诊断**永远不可能出现**。 已加看门狗(20s,小于 testTimeout 30s)并放宽外层。 ## 验证(三层退出码均 0) - typecheck 0 - 单测 **951 passed** - live 全套 **20 passed / 4 files**;各场景满分:readonly 8/8、其余 3/3、方向专项 2/2 - CLI 真机 3 turn、零失败调用 - eval set 44 → 46,新增第十一类「依赖方向」(upward-import / leaked-upper-type, 落在 `src/graph/{orchestrator,plan}.ts`)。这一类与 layering 那一类的区别: 那边判据写在 README 的架构约定里,这边写在**被违反的那个文件自己的 JSDoc 里**, 违反痕迹只有一行 import——不看另一个文件就不知道那是"上层"。 新场景 `dependency-direction-review` 命中 2/2。
在飞的节点收不到编排的中断:`runWorkflow` 只在派每一批之前查 `signal.aborted`, 挡得住"还没启动的层",但 `NodeRunInput` 里没有 signal 字段,已经启动的节点用什么 信号完全取决于调用方 —— kernel 侧 `runWorkflowNode` 传的是 `toolCtx.signal` (主会话那次工具调用的信号),不是编排自己的。 真机后果:后台派一个 workflow,等节点跑起来后 kill 编排,8 秒后编排已是 killed 而 慢节点仍停在 `running`,既没被杀也没跑完,还在烧 token。 改动:`NodeRunInput` 加必填 `signal`,`runWorkflow` 填 `control.signal`,kernel 侧 用它取代 `toolCtx.signal`。后者是前者的上游(`track()` 把编排的 AbortController 链在 `toolCtx.signal` 下),所以这是严格超集:主会话中断照样传到,另外多覆盖"编排自己被 kill"。 同一真机场景复测:编排 killed,慢节点 running → killed,已跑完的快节点保留 completed。 顺带修一条 live 假红:`derived-agent` 的跨层取值断言原先断在 `res.output` 上, 那要求终止节点的模型把上游值转述出来,是模型行为而非代码属性 —— 本地网关已见第五种 把 tool call 当纯文本吐出来的形态(`<tool_calls>`)。改断 joiner 的 prompt,回到 `buildNodeInput` 的代码属性上,对模型措辞免疫(变异掉拼接仍变红,已实测)。 测试 - 新增 packages/workflow/test/kill-propagation.test.ts(3 例:层间闸门、signal 传到每个 节点、在飞节点收到中断),3/3 变异全被抓住 - eval set 46 → 49:新增"取消信号传播"类(src/tasks/pipeline.ts 三个缺陷)与 cancel-propagation-review 场景,首跑 2/3(漏 abort-before-listen) - typecheck 0 错 · 单测 954 passed · live 全量 20 passed · CLI 真机 workflow 跑通
原注释写「8 秒后节点仍 running / 复测已 killed」,把一个**同步**保证描述成了最终一致。 探针里的 8s 是我随手挑的,不是从任何超时推出来的:修复前它证明「等 8s 仍没停」(这个 方向成立),修复后它证明「8s 内停了」(这个方向什么都没说,真实延迟可能是 50ms)。 实际路径是同步的:kill 编排 → abort.abort() → 同 tick 派发 abort 事件 → track() 的 onAbort 调 kill(nodeId) → markEnd 立刻标 killed,不需要等待。 同时划清射程:本文件保证的是调度器把信号递到了、记录状态同步翻转;信号递到之后那次 LLM 请求与 Bash 子进程各自多久真正停下,是执行器与工具的事。
fix(workflow): kill 编排时把中断信号传给在飞的节点
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
先说 A-9 的前提只对了一半
原表述:「
workflow.ts落在packages/kernel/src/agentSpec/,但 DAG 编排比 session 高一层,kernel 的职责是『跑一个 run』」。查下来不成立。workflow 不是 kernel 的上层,它是 kernel 注册给模型的一个工具(
agentSpec/agentTools.ts),运行时嵌在 session 里、靠会话级DerivedAgentExecutor干活。而且照字面搬会包级循环依赖:
workflow.ts的唯一调用方在 kernel 内,而它自己又反过来 import kernel 的load/validate/selectTools/portResolver/derivedAgentExecutor。所以这期只剥出纯算法层。把它抬到 kernel 之上(做成 extension)依赖 A-8 ——
agentTools.ts现在靠executorFor: (sessionId) => …取会话级执行器,那正是 A-8 要删的 service locator。已在docs/follow-up-work.md把 A-9 标为「部分 DONE」并记下这条依赖。做了什么
新包
@helios/workflow,依赖只有@helios/ports,方向kernel → workflow单向:compile.tsschedule.tsWorkflowControl/NodeOutcome/WorkflowRunContext),不 import kernel 的类三个共用纯函数落到
ports/agentSpec.ts(两个包都要用,公共部分只能放共同下层,与skill.ts的parseFrontmatter同一先例):isWorkflowSpec/resolveDefinition/workflowNodeId。workflowNodeId带前缀是刻意的 ——nodeId在session.ts里指消息树节点,同名不同义。顺带收紧两处:
CompileContext的allTools+portResolver两个字段收成一个checkAssembly。「这个 agent 装得起来吗」是 kernel 的知识(它才持有工具池与 Port 白名单),workflow 只要一个「是/否 + 原因」。字段变少了,边界反而更清楚。WorkflowRunContext.executor是死字段,runWorkflow从来没用过它(测试里一路executor: {} as never传着)。删掉。护栏
packages/workflow/test/dependency-direction.test.ts—— 对源码 import 与 package.json 的静态断言,含一条「护栏本身不是空的」自检(防readdir指错目录时前两条永远绿)。@helios/kernel本来就解析得到,TS 会愉快编过去,只在拆包 / 发版 / 拓扑构建时才炸。变异 9/9 命中:源码 import kernel / package.json 加 kernel 依赖 / 护栏扫错目录 / 分层按完成顺序 / 多终止节点放过 / 上游拼接乱序 / 节点失败仍跑下游 /
checkAssembly不调 / 嵌套深度上限失效。顺带修掉三处测试质量问题
这三处都是这轮跑回归时被逼出来的,不是顺手重构。
① workflow live 用例长期假红
它把代码属性和模型能力混在一条断言里(
toContain("PINEAPPLE-42")同时要求「数据沿 DAG 流动」和「叶子 agent 会去调 Read」)。补两条确定性断言:joiner 的 prompt 里确实有
<upstream node="pw">块 —— 那正是本次搬走的buildNodeInput产出的。然后按语义判据分流:pw自己都没拿到口令 → 叶子没干活(模型问题)→skip并打印原因不按标记形态识别是吃过教训的:实测已见四种形态(
<|DSML|invoke>/<use_mcp_tool>/<function_calls>/<tool_use>),我追加过一轮又漏了第四种 —— 那是无穷枚举。验证 skip 没吞掉真回归:变异掉
buildNodeInput的拼接后,用例仍在<upstream>断言上变红。② eval 的分母一直是错的
以前一律拿全集
DEFECTS.length作分母,于是每轮往 eval set 加缺陷都会把所有场景的分数稀释一遍。我曾把39/41 → 41/44 → 24/46当趋势报给你 —— 那是三个不同的量,denominator 和被审文件集都在变,不可比。这是我的测量错误。现在各场景按自己的文件范围算分母(分子也要过滤,否则打出
10/8这种数字)。修完各场景是满分:另外 readonly 广度扫描已经撞了 600s 上限被截断(单跑必超时,批内勉强跑完但结论不全)—— 那才是「24/46」的真因,不是模型变差。范围钉死到 4 个文件;它只负责回答「一次广度扫描能不能捞到有意义的东西」,新类别请配套新增专项场景而不是往这个 scope 里加文件。
③ CLI e2e 两个用例间歇假红
runCliArgs完全没有内部看门狗,子进程不退出就靠 vitest 的 5s 兜底 —— 而那条路径不杀子进程,留下一个还在跑的 tsx 孤儿,报错也只有无信息量的「Test timed out」。configDiscovery更荒谬:内部看门狗 10s > 外层 5s,于是那句精心写的诊断(CLI did not become ready. Output:...)永远不可能出现。内层预算大于外层 = 把诊断关掉了。已加看门狗(20s,小于 testTimeout 30s)并放宽外层。
验证(三层退出码均为 0)
eval set 44 → 46,新增第十一类「依赖方向」:
upward-importplan.ts(底层图算法)import 了上层orchestrator,形成相互依赖leaked-upper-typecompile收了上层整个Orchestrator,实际只用它一个方法与
layering-review那一类的区别:那边判据写在 README 的架构约定里,这边写在被违反的那个文件自己的 JSDoc 里,而违反的痕迹只有一行 import —— 不看另一个文件就不知道那是"上层"。新场景dependency-direction-review命中 2/2。