@zhushanwen/subagent-core 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/README.md +45 -2
  2. package/agents/analyst.md +60 -0
  3. package/agents/coder.md +69 -0
  4. package/agents/debugger.md +66 -0
  5. package/agents/doc-reviewer.md +49 -0
  6. package/agents/explorer.md +63 -0
  7. package/agents/general-purpose.md +32 -0
  8. package/agents/orchestrator.md +61 -0
  9. package/agents/planner.md +53 -0
  10. package/agents/researcher.md +67 -0
  11. package/agents/reviewer.md +73 -0
  12. package/dist/chunk-4KN4TTG7.js +240 -0
  13. package/dist/chunk-APZY4IME.js +27 -0
  14. package/dist/chunk-X7SZ5HLQ.js +66 -0
  15. package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
  16. package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
  17. package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
  18. package/dist/execution/engine/engines/zcode/constants.js +43 -1
  19. package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
  20. package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
  21. package/dist/execution/engine/engines/zcode/reader.js +4 -234
  22. package/dist/execution/engine/paths.js +7 -19
  23. package/dist/index.cjs +14709 -100
  24. package/dist/index.d.cts +4053 -200
  25. package/dist/index.d.ts +4053 -200
  26. package/dist/index.js +14358 -102
  27. package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
  28. package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
  29. package/package.json +6 -3
  30. package/src/__tests__/review-fix-loop-script.test.ts +90 -2
  31. package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
  32. package/src/__tests__/smoke.test.ts +9 -2
  33. package/src/execution/__tests__/agent-profile.test.ts +232 -0
  34. package/src/execution/__tests__/agents-assembly.test.ts +219 -0
  35. package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
  36. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
  37. package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
  38. package/src/execution/__tests__/delivery-methods.test.ts +2 -0
  39. package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
  40. package/src/execution/__tests__/dialog-queue.test.ts +199 -1
  41. package/src/execution/__tests__/epipe-fallback.test.ts +2 -0
  42. package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
  43. package/src/execution/__tests__/finalize-record.test.ts +106 -0
  44. package/src/execution/__tests__/gc-timer.test.ts +2 -0
  45. package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
  46. package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
  47. package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -0
  48. package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
  49. package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
  50. package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
  51. package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
  52. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
  53. package/src/execution/__tests__/notify-ledger.test.ts +294 -0
  54. package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
  55. package/src/execution/__tests__/record-store.test.ts +83 -0
  56. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
  57. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
  58. package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
  59. package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
  60. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
  61. package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
  62. package/src/execution/__tests__/session-pending.test.ts +228 -0
  63. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
  64. package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
  65. package/src/execution/__tests__/session-runner-epipe.test.ts +1 -0
  66. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
  67. package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
  68. package/src/execution/__tests__/spawned-children.test.ts +153 -10
  69. package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
  70. package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
  71. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
  72. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
  73. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
  74. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
  75. package/src/execution/__tests__/timeout-integration.test.ts +7 -7
  76. package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
  77. package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
  78. package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
  79. package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
  80. package/src/execution/agent-registry.ts +225 -1
  81. package/src/execution/agents-assembly.ts +88 -0
  82. package/src/execution/concurrency-pool.ts +71 -9
  83. package/src/execution/dialog-queue.ts +101 -3
  84. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
  85. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
  86. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
  87. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
  88. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
  89. package/src/execution/engine/__tests__/registry.test.ts +90 -1
  90. package/src/execution/engine/engine-discovery.ts +12 -16
  91. package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
  92. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
  93. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
  94. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
  95. package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
  96. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
  97. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
  98. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
  99. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
  100. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
  101. package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
  102. package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
  103. package/src/execution/engine/engines/zcode/connection.ts +585 -0
  104. package/src/execution/engine/engines/zcode/constants.ts +129 -0
  105. package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
  106. package/src/execution/engine/engines/zcode/preparer.ts +25 -22
  107. package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
  108. package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
  109. package/src/execution/engine/host-task-spec.ts +3 -3
  110. package/src/execution/engine/port.ts +41 -1
  111. package/src/execution/engine/registry.ts +53 -0
  112. package/src/execution/finalize-record.ts +63 -5
  113. package/src/execution/get-state-handshake.ts +85 -10
  114. package/src/execution/lifecycle-manager.ts +27 -3
  115. package/src/execution/manifest-store.ts +53 -62
  116. package/src/execution/notifier.ts +10 -10
  117. package/src/execution/notify-ledger.ts +117 -16
  118. package/src/execution/record-entry.ts +8 -2
  119. package/src/execution/record-store.ts +29 -15
  120. package/src/execution/session-pending.ts +213 -62
  121. package/src/execution/session-runner.ts +974 -128
  122. package/src/execution/sessions-index.ts +10 -55
  123. package/src/execution/settled-watchdog.ts +99 -0
  124. package/src/execution/subagent-actions-core.ts +686 -0
  125. package/src/execution/subagent-service.ts +396 -49
  126. package/src/execution/ui-request-handler-factory.ts +27 -4
  127. package/src/execution/worktree-git-ops.ts +397 -0
  128. package/src/execution/worktree-manager.ts +72 -10
  129. package/src/execution/worktree-registry.ts +6 -12
  130. package/src/index.ts +413 -6
  131. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
  132. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
  133. package/src/orchestration/__tests__/args-meta.test.ts +358 -0
  134. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
  135. package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
  136. package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
  137. package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
  138. package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
  139. package/src/orchestration/__tests__/file-run-store.test.ts +389 -0
  140. package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
  141. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
  142. package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
  143. package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
  144. package/src/orchestration/__tests__/lifecycle.test.ts +77 -0
  145. package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
  146. package/src/orchestration/__tests__/script-generate.test.ts +314 -0
  147. package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
  148. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
  149. package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
  150. package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
  151. package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
  152. package/src/orchestration/agent-opts-resolver.ts +7 -7
  153. package/src/orchestration/args-meta.ts +198 -0
  154. package/src/orchestration/error-recovery.ts +416 -146
  155. package/src/orchestration/execute-agent-call.ts +9 -9
  156. package/src/orchestration/file-run-store.ts +327 -0
  157. package/src/orchestration/launcher.ts +35 -26
  158. package/src/orchestration/lifecycle.ts +355 -79
  159. package/src/orchestration/models/agent-call.ts +7 -7
  160. package/src/orchestration/models/budget.ts +5 -5
  161. package/src/orchestration/models/run-runtime.ts +13 -13
  162. package/src/orchestration/models/trace.ts +8 -8
  163. package/src/orchestration/models/workflow-run.ts +27 -27
  164. package/src/orchestration/models/workflow-script.ts +4 -4
  165. package/src/orchestration/run-snapshot.ts +266 -0
  166. package/src/orchestration/script-generate.ts +154 -0
  167. package/src/orchestration/script-lint.ts +33 -33
  168. package/src/orchestration/worker-handle.ts +10 -10
  169. package/src/orchestration/worker-host.ts +10 -10
  170. package/src/orchestration/worker-script-builder.ts +401 -346
  171. package/src/orchestration/workflow-files.ts +37 -11
  172. package/src/orchestration/workflow-run-summary.ts +69 -0
  173. package/src/orchestration/workflow-script-registry-impl.ts +31 -9
  174. package/src/shared/__tests__/agent-ref.test.ts +209 -2
  175. package/src/shared/__tests__/atomic-write.test.ts +267 -0
  176. package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
  177. package/src/shared/__tests__/injection-render.test.ts +518 -0
  178. package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
  179. package/src/shared/__tests__/resource-discovery.test.ts +3 -2
  180. package/src/shared/agent-ref.ts +143 -2
  181. package/src/shared/atomic-write.ts +320 -0
  182. package/src/shared/bounded-serialize.ts +154 -0
  183. package/src/shared/injection-render.ts +279 -0
  184. package/src/shared/meta-parser.ts +41 -1
  185. package/src/shared/resource-discovery.ts +104 -37
  186. package/src/shared/resource-meta.ts +14 -0
  187. package/src/shared/xml-injection.ts +9 -9
  188. package/workflows/README.md +9 -9
  189. package/workflows/chain.js +4 -2
  190. package/workflows/map-reduce.js +5 -3
  191. package/workflows/parallel.js +5 -3
  192. package/workflows/review-fix-loop-utils.cjs +14 -3
  193. package/workflows/review-fix-loop.js +18 -7
  194. package/workflows/scatter-gather.js +4 -2
  195. package/dist/chunk-3VOERJPJ.js +0 -22
@@ -251,6 +251,44 @@ describe("runWorkflow", () => {
251
251
  expect(deps.eventBus.emit).not.toHaveBeenCalled();
252
252
  });
253
253
 
254
+ // OR-1 顺序修复(unbounded-wait-audit §4.1):budgetTimeMs 溢出的 fail-fast
255
+ // 必须发生在首个副作用(workerHost.start)之前——旧顺序 worker 先启动、
256
+ // scheduleTimeBudget 后 throw,产生「worker 已跑、run 永不可达」的孤儿。
257
+ it("OR-1: budgetTimeMs 溢出(>2^31-1)→ worker 启动前 fail-fast(zero side effects)", async () => {
258
+ const deps = makeDeps();
259
+ const spec = makeSpec({ budgetTimeMs: 3_000_000_000 });
260
+
261
+ await expect(runWorkflow(spec, deps)).rejects.toThrow(/2147483647/);
262
+
263
+ // fail-fast 先于全部副作用:worker 未启动、run 未注册、未落盘、未通知
264
+ expect(deps.workerHost.start).not.toHaveBeenCalled();
265
+ expect(deps.runs.size).toBe(0);
266
+ expect(deps.store.save).not.toHaveBeenCalled();
267
+ expect(deps.eventBus.emit).not.toHaveBeenCalled();
268
+ });
269
+
270
+ // OR-1 前移的代价:start 抛错时已挂的 timeBudget timer 会残留(到期对从未注册的
271
+ // runId 触发 abortRun → not found)——start 抛错路径必须就地 clearTimeout。
272
+ it("OR-1: budgetTimeMs 有效但 worker.start 抛错 → timeBudget timer 被清理(无残留)", async () => {
273
+ const deps = makeDeps();
274
+ deps.workerHost.start = vi.fn(() => {
275
+ throw new Error("worker boot failed");
276
+ });
277
+
278
+ // fake timers:用例开始时无 pending 计时器(beforeEach 全新安装)
279
+ expect(vi.getTimerCount()).toBe(0);
280
+
281
+ await expect(
282
+ runWorkflow(makeSpec({ budgetTimeMs: 60_000 }), deps),
283
+ ).rejects.toThrow("worker boot failed");
284
+
285
+ // timer 挂载(scheduleTimeBudget)发生在 start 之前 → 抛错路径必须回收:
286
+ // 无 pending 计时器残留(若漏清理此处为 1,到期会误 abort 幽灵 runId)
287
+ expect(vi.getTimerCount()).toBe(0);
288
+ // 既有行为不回归:runs Map 无该 runId 条目(abortRun not found 语义保持)
289
+ expect(deps.runs.size).toBe(0);
290
+ });
291
+
254
292
  it("带 budgetTimeMs 时调度时间预算计时器(真实消费:挂载 + 到期 time_limited)", async () => {
255
293
  const deps = makeDeps();
256
294
  const spec = makeSpec({ budgetTimeMs: 3000 });
@@ -359,6 +397,25 @@ describe("abortRun", () => {
359
397
  expect(deps.onRunDone).not.toHaveBeenCalled();
360
398
  });
361
399
 
400
+ it("[B-4] eventBus listener 抛错 → abortRun 不向调用方抛错、onRunDone 仍执行", async () => {
401
+ const { run, terminate } = makeRunningRealRun("wf-abort-fence");
402
+ const deps = makeDeps();
403
+ deps.eventBus.emit = vi.fn(() => {
404
+ throw new Error("listener exploded");
405
+ });
406
+ deps.runs.set("wf-abort-fence", run);
407
+
408
+ // 旧实现裸调 emit:listener 抛错 → 异常上抛给 tool 调用方 + 跳过 onRunDone
409
+ await expect(abortRun("wf-abort-fence", deps, "abort with broken bus")).resolves.toBeUndefined();
410
+
411
+ // abort 成功语义保持:终态转换/terminate 照常完成
412
+ expect(run.state.status).toBe("done");
413
+ expect(run.state.reason).toBe("aborted");
414
+ expect(terminate).toHaveBeenCalledTimes(1);
415
+ // 独立围栏:onRunDone 不被 emit 故障跳过
416
+ expect(deps.onRunDone).toHaveBeenCalledTimes(1);
417
+ });
418
+
362
419
  it("自定义 doneReason(time_limited)", async () => {
363
420
  const { run } = makeRunningRealRun("wf-abort-3");
364
421
  const deps = makeDeps();
@@ -436,6 +493,26 @@ describe("terminateRunningRuns", () => {
436
493
  expect(deps.store.save).toHaveBeenCalledWith(run);
437
494
  });
438
495
 
496
+ it("[B-4] eventBus listener 抛错 → 不向调用方抛错、本 run 收尾完成、其余 run 继续终止", async () => {
497
+ const { run: a, terminate: terminateA } = makeRunningRealRun("wf-term-fence-a");
498
+ const { run: b, terminate: terminateB } = makeRunningRealRun("wf-term-fence-b");
499
+ const deps = makeDeps();
500
+ deps.runs.set("wf-term-fence-a", a);
501
+ deps.runs.set("wf-term-fence-b", b);
502
+ // 仅 runA 的 emit 抛错(按 payload id 判别模拟坏 listener)
503
+ deps.eventBus.emit = vi.fn((_event: string, payload: { id: string }) => {
504
+ if (payload.id === "wf-term-fence-a") throw new Error("listener exploded");
505
+ });
506
+
507
+ await expect(terminateRunningRuns(deps, "Session switched: run terminated")).resolves.toBeUndefined();
508
+
509
+ // 两个 run 都走到终态(runA 的 emit 故障不中断批量收尾)
510
+ expect(a.state.reason).toBe("failed");
511
+ expect(b.state.reason).toBe("failed");
512
+ expect(terminateA).toHaveBeenCalledTimes(1);
513
+ expect(terminateB).toHaveBeenCalledTimes(1);
514
+ });
515
+
439
516
  it("单 run save 抛错不中断其余(其余 run 仍落盘 + unregister)", async () => {
440
517
  const { run: bad } = makeRunningRealRun("wf-term-err");
441
518
  const { run: good } = makeRunningRealRun("wf-term-ok");
@@ -0,0 +1,353 @@
1
+ // run-snapshot.test.ts —— WorkflowRun 快照 codec 单源(下沉收口 D4/U8)。
2
+ //
3
+ // 四视角:
4
+ // ①使用者——toRunSnapshot/fromRunSnapshot 往返等值(含 pi 壳 jsonl-run-store
5
+ // serializeRun 现网形态样本:键序/v 字段/无 live 的快照行,⛔5 往返逐字节一致);
6
+ // ②隔离者——live-strip 产出新对象,不 mutate 内存中的 run(save 后 run 可继续跑);
7
+ // ③幸存者——版本 guard(D4 裁决③:v 不匹配即拒,字符串无大小序)+ 形状校验
8
+ // 全分支不抛(返回 undefined);
9
+ // ④接线者——「缺 v 宽容」不内聚进 codec(D4 裁决②归属:store 层预处理职责,
10
+ // codec 层缺 v 即拒,保 pi 侧 v1 存量静默跳过语义)+ spec.budgetRef 剔除。
11
+ //
12
+ // 纯内存测试:无 configureCore 依赖(codec 不触 host-services)。
13
+
14
+ import { describe, expect, it } from "vitest";
15
+
16
+ import type { ExecutionRecord } from "../../execution/types.ts";
17
+ import { AgentCall } from "../models/agent-call.ts";
18
+ import { Budget } from "../models/budget.ts";
19
+ import { Trace } from "../models/trace.ts";
20
+ import { WorkflowRun } from "../models/workflow-run.ts";
21
+ import { SNAPSHOT_VERSION, fromRunSnapshot, toRunSnapshot } from "../run-snapshot.ts";
22
+
23
+ /** 构造可持久化的 WorkflowRun(对齐 file-run-store.test.ts makeRun 模式)。 */
24
+ function makeRun(runId: string, opts: { status?: "running" | "done" } = {}): WorkflowRun {
25
+ const status = opts.status ?? "running";
26
+ return WorkflowRun.reconstruct(
27
+ runId,
28
+ {
29
+ scriptSource: "export function execute() { return 'ok'; }",
30
+ args: { topic: "demo", count: 2 },
31
+ scriptName: "test-script",
32
+ scriptPath: "/fake/test.js",
33
+ parameters: { type: "object" },
34
+ budgetTokens: 1000,
35
+ },
36
+ {
37
+ status,
38
+ ...(status === "done" ? { reason: "completed" as const } : {}),
39
+ budget: new Budget({ maxTokens: 1000, usedTokens: 42, usedCost: 0.5, totalCallCount: 3 }),
40
+ calls: new Map(),
41
+ trace: new Trace(),
42
+ errorLogs: [],
43
+ scriptResult: status === "done" ? { summary: "done-value" } : undefined,
44
+ },
45
+ { startedAt: "2026-08-30T00:00:00.000Z" },
46
+ );
47
+ }
48
+
49
+ /** 最小 live 执行进度对象(strip 逻辑只解构键名,值形态不可达——对齐
50
+ * lifecycle-predicates.test.ts 的 partial + as ExecutionRecord 先例)。 */
51
+ function makeLiveRecord(id: string): ExecutionRecord {
52
+ return {
53
+ id,
54
+ agent: "coder",
55
+ model: "test-model",
56
+ thinkingLevel: undefined,
57
+ mode: "sync",
58
+ task: "do work",
59
+ slug: "do-work",
60
+ startedAt: 0,
61
+ rootSessionId: undefined,
62
+ parentRecordId: undefined,
63
+ depth: 0,
64
+ status: "running",
65
+ turns: [],
66
+ turnCount: 0,
67
+ totalTokens: 0,
68
+ lastError: undefined,
69
+ round: 0,
70
+ endedAt: undefined,
71
+ result: undefined,
72
+ error: undefined,
73
+ agentResult: undefined,
74
+ controller: undefined,
75
+ } as ExecutionRecord;
76
+ }
77
+
78
+ /**
79
+ * pi 壳 jsonl-run-store.ts serializeRun 现网形态样本(顶层 done 前的 running
80
+ * run,字段集/键序按其对象字面量排列)。pi 存量行恒无 live(其 serializeRun
81
+ * strip)、恒带 v="wf-run-v2"(D4 裁决①:版本值沿用)。
82
+ */
83
+ const PI_FORM_SNAPSHOT = {
84
+ v: "wf-run-v2",
85
+ runId: "wf-pi-form-1",
86
+ spec: {
87
+ scriptSource: "export function execute() { return 'pi-form'; }",
88
+ args: { topic: "demo" },
89
+ scriptName: "test-script",
90
+ scriptPath: "/fake/test.js",
91
+ budgetTokens: 1000,
92
+ },
93
+ state: {
94
+ status: "running",
95
+ budget: { maxTokens: 1000, usedTokens: 42, usedCost: 0.5, totalCallCount: 3 },
96
+ calls: [
97
+ {
98
+ id: 0,
99
+ opts: { prompt: "do work" },
100
+ status: "done",
101
+ attempts: 1,
102
+ result: { content: "ok" },
103
+ sessionId: "sess-1",
104
+ sessionFile: "/tmp/sess-1.jsonl",
105
+ traceNode: {
106
+ stepIndex: 0,
107
+ agent: "coder",
108
+ task: "do work",
109
+ model: "test-model",
110
+ status: "completed",
111
+ startedAt: "2026-08-30T00:00:01.000Z",
112
+ completedAt: "2026-08-30T00:00:02.000Z",
113
+ sessionId: "sess-1",
114
+ sessionFile: "/tmp/sess-1.jsonl",
115
+ },
116
+ },
117
+ ],
118
+ trace: [
119
+ {
120
+ stepIndex: 0,
121
+ agent: "coder",
122
+ task: "do work",
123
+ model: "test-model",
124
+ status: "completed",
125
+ startedAt: "2026-08-30T00:00:01.000Z",
126
+ completedAt: "2026-08-30T00:00:02.000Z",
127
+ sessionId: "sess-1",
128
+ sessionFile: "/tmp/sess-1.jsonl",
129
+ },
130
+ ],
131
+ errorLogs: [{ level: "warn", message: "retrying" }],
132
+ },
133
+ meta: { startedAt: "2026-08-30T00:00:00.000Z" },
134
+ };
135
+
136
+ describe("run-snapshot — toRunSnapshot/fromRunSnapshot 往返等值", () => {
137
+ it("⛔5 pi 现网形态样本:重水合 → 再序列化与原行逐字节一致(v 字段保持 wf-run-v2)", () => {
138
+ const line = JSON.stringify(PI_FORM_SNAPSHOT);
139
+
140
+ const run = fromRunSnapshot(JSON.parse(line));
141
+ expect(run).toBeDefined();
142
+ expect(run!.runId).toBe("wf-pi-form-1");
143
+
144
+ // 逐字节一致:pi 切换本 codec 后存量行往返不变(u-sw-store ⛔5 前提)
145
+ expect(JSON.stringify(toRunSnapshot(run!))).toBe(line);
146
+ });
147
+
148
+ it("running 快照往返:runId/spec/budget/trace/meta 字段保真", () => {
149
+ const run = makeRun("wf-snap-1");
150
+ const back = fromRunSnapshot(JSON.parse(JSON.stringify(toRunSnapshot(run))));
151
+
152
+ expect(back).toBeDefined();
153
+ expect(back!.runId).toBe("wf-snap-1");
154
+ expect(back!.state.status).toBe("running");
155
+ expect(back!.spec.scriptName).toBe("test-script");
156
+ expect(back!.spec.args).toEqual({ topic: "demo", count: 2 });
157
+ expect(back!.state.budget).toBeInstanceOf(Budget);
158
+ expect(back!.state.budget.usedTokens).toBe(42);
159
+ expect(back!.state.budget.totalCallCount).toBe(3);
160
+ expect(back!.state.trace).toBeInstanceOf(Trace);
161
+ expect(back!.runtime).toBeUndefined();
162
+ expect(back!.meta.startedAt).toBe("2026-08-30T00:00:00.000Z");
163
+ });
164
+
165
+ it("done 快照往返:reason/scriptResult/errorLogs/completedAt 保真", () => {
166
+ const run = makeRun("wf-snap-2", { status: "done" });
167
+ run.state.errorLogs.push({ level: "warn", message: "transient" });
168
+ run.meta.workerErrorCount = 1;
169
+
170
+ const back = fromRunSnapshot(JSON.parse(JSON.stringify(toRunSnapshot(run))));
171
+
172
+ expect(back!.state.status).toBe("done");
173
+ expect(back!.state.reason).toBe("completed");
174
+ expect(back!.state.scriptResult).toEqual({ summary: "done-value" });
175
+ expect(back!.state.errorLogs).toEqual([{ level: "warn", message: "transient" }]);
176
+ expect(back!.meta.completedAt).toBe(run.meta.completedAt);
177
+ expect(back!.meta.workerErrorCount).toBe(1);
178
+ });
179
+
180
+ it("含 calls 的往返:calls Map 逐项保真 + traceNode 回链 Trace 副本(D-10 尽力恢复)", () => {
181
+ const run = makeRun("wf-snap-3");
182
+ const node = {
183
+ stepIndex: 0,
184
+ agent: "coder",
185
+ task: "do work",
186
+ model: "test-model",
187
+ status: "completed" as const,
188
+ };
189
+ run.state.trace.append(node);
190
+ const call = new AgentCall(0, { prompt: "do work" }, node);
191
+ call.status = "done";
192
+ call.attempts = 1;
193
+ run.state.calls.set(0, call);
194
+
195
+ const back = fromRunSnapshot(JSON.parse(JSON.stringify(toRunSnapshot(run))))!;
196
+
197
+ expect(back.state.calls.size).toBe(1);
198
+ const restored = back.state.calls.get(0)!;
199
+ expect(restored.opts.prompt).toBe("do work");
200
+ expect(restored.status).toBe("done");
201
+ expect(restored.attempts).toBe(1);
202
+ expect(restored.traceNode).toBe(back.state.trace.toArray()[0]);
203
+ });
204
+ });
205
+
206
+ describe("run-snapshot — live-strip 防御内聚", () => {
207
+ it("trace 节点与 calls[].traceNode 带 live → 序列化输出无 live 键", () => {
208
+ const run = makeRun("wf-live-1");
209
+ const node = {
210
+ stepIndex: 0,
211
+ agent: "coder",
212
+ task: "do work",
213
+ model: "test-model",
214
+ status: "running" as const,
215
+ };
216
+ run.state.trace.append(node);
217
+ const call = new AgentCall(0, { prompt: "do work" }, node);
218
+ run.state.calls.set(0, call);
219
+ // D-10:call.traceNode 与 trace 节点共享引用,live 挂节点上(running 态常态)
220
+ node.live = makeLiveRecord("run-0");
221
+
222
+ const snap = toRunSnapshot(run);
223
+ const serialized = JSON.stringify(snap);
224
+
225
+ expect(serialized.includes('"live"')).toBe(false);
226
+ expect(snap.state.trace[0]).not.toHaveProperty("live");
227
+ expect(snap.state.calls[0].traceNode).not.toHaveProperty("live");
228
+ });
229
+
230
+ it("strip 产出新对象,不 mutate 内存 run(save 后 run 可继续跑)", () => {
231
+ const run = makeRun("wf-live-2");
232
+ const node = {
233
+ stepIndex: 0,
234
+ agent: "coder",
235
+ task: "do work",
236
+ model: "test-model",
237
+ status: "running" as const,
238
+ };
239
+ run.state.trace.append(node);
240
+ node.live = makeLiveRecord("run-0");
241
+
242
+ toRunSnapshot(run);
243
+
244
+ // 原节点 live 保留(运行期消费面——TUI 事件流投影——不受落盘影响)
245
+ expect(node.live).toBeDefined();
246
+ expect(node.live!.id).toBe("run-0");
247
+ });
248
+ });
249
+
250
+ describe("run-snapshot — spec.budgetRef 剔除", () => {
251
+ it("spec.budgetRef(进程内共享引用)不落盘,spec 其余字段保真", () => {
252
+ const run = makeRun("wf-ref-1");
253
+ const specWithRef = { ...run.spec, budgetRef: new Budget({ maxTokens: 500 }) };
254
+ // reconstruct 直造带 budgetRef 的聚合(嵌套 workflow 的 run 形态)
255
+ const nested = WorkflowRun.reconstruct("wf-ref-1", specWithRef, run.state, run.meta);
256
+
257
+ const snap = toRunSnapshot(nested);
258
+
259
+ expect(nested.spec.budgetRef).toBeDefined(); // 内存对象不受影响
260
+ expect(snap.spec).not.toHaveProperty("budgetRef");
261
+ expect(snap.spec.scriptName).toBe("test-script");
262
+
263
+ const back = fromRunSnapshot(JSON.parse(JSON.stringify(snap)))!;
264
+ expect(back.spec).not.toHaveProperty("budgetRef");
265
+ expect(back.spec.budgetTokens).toBe(1000);
266
+ });
267
+ });
268
+
269
+ describe("run-snapshot — 版本 guard(D4 裁决③)", () => {
270
+ it(`v = 当前版本(${SNAPSHOT_VERSION})通过`, () => {
271
+ const run = fromRunSnapshot(JSON.parse(JSON.stringify(toRunSnapshot(makeRun("wf-v-ok")))));
272
+ expect(run).toBeDefined();
273
+ });
274
+
275
+ it("未知更高版本(wf-run-v3)拒绝——字符串版本无大小序,不引入比较逻辑", () => {
276
+ const snap = { ...JSON.parse(JSON.stringify(toRunSnapshot(makeRun("wf-v-3")))), v: "wf-run-v3" };
277
+ expect(fromRunSnapshot(snap)).toBeUndefined();
278
+ });
279
+
280
+ it("旧版本 wf-run-v1 拒绝(pi 存量静默跳过语义的数据防线)", () => {
281
+ const snap = { ...JSON.parse(JSON.stringify(toRunSnapshot(makeRun("wf-v-1")))), v: "wf-run-v1" };
282
+ expect(fromRunSnapshot(snap)).toBeUndefined();
283
+ });
284
+
285
+ it("缺 v 字段拒绝——「缺 v 宽容」是 store 层预处理职责,不内聚进 codec(D4 裁决②)", () => {
286
+ const { v: _v, ...legacy } = JSON.parse(JSON.stringify(toRunSnapshot(makeRun("wf-v-none"))));
287
+ expect(fromRunSnapshot(legacy)).toBeUndefined();
288
+ });
289
+
290
+ it("v 为非字符串脏值(数字/null)拒绝", () => {
291
+ const base = JSON.parse(JSON.stringify(toRunSnapshot(makeRun("wf-v-dirty"))));
292
+ expect(fromRunSnapshot({ ...base, v: 2 })).toBeUndefined();
293
+ expect(fromRunSnapshot({ ...base, v: null })).toBeUndefined();
294
+ });
295
+ });
296
+
297
+ describe("run-snapshot — 形状校验(损坏行不抛,返回 undefined)", () => {
298
+ it("null / 非对象 / 缺 runId / 空 runId 拒绝", () => {
299
+ expect(fromRunSnapshot(null)).toBeUndefined();
300
+ expect(fromRunSnapshot("not an object")).toBeUndefined();
301
+ expect(fromRunSnapshot({ v: SNAPSHOT_VERSION, spec: {}, state: {}, meta: {} })).toBeUndefined();
302
+ expect(fromRunSnapshot({ v: SNAPSHOT_VERSION, runId: "", spec: {}, state: {}, meta: {} })).toBeUndefined();
303
+ });
304
+
305
+ it("缺 spec / 缺 state / 非法 status / 缺 budget / 缺 calls / 缺 trace / 缺 meta 拒绝", () => {
306
+ const ok = () => JSON.parse(JSON.stringify(toRunSnapshot(makeRun("wf-shape-base"))));
307
+ const without = (keys: string[]) => {
308
+ const snap = ok();
309
+ for (const k of keys) delete snap[k];
310
+ return snap;
311
+ };
312
+
313
+ expect(fromRunSnapshot(without(["spec"]))).toBeUndefined();
314
+ expect(fromRunSnapshot(without(["state"]))).toBeUndefined();
315
+ expect(fromRunSnapshot(without(["meta"]))).toBeUndefined();
316
+
317
+ const badStatus = ok();
318
+ badStatus.state.status = "paused"; // v1 三态残留在 v2 快照 = 形状损坏
319
+ expect(fromRunSnapshot(badStatus)).toBeUndefined();
320
+
321
+ const noBudget = ok();
322
+ delete noBudget.state.budget;
323
+ expect(fromRunSnapshot(noBudget)).toBeUndefined();
324
+
325
+ const noCalls = ok();
326
+ delete noCalls.state.calls;
327
+ expect(fromRunSnapshot(noCalls)).toBeUndefined();
328
+
329
+ const noTrace = ok();
330
+ delete noTrace.state.trace;
331
+ expect(fromRunSnapshot(noTrace)).toBeUndefined();
332
+ });
333
+
334
+ it("calls 内残缺条目跳过不炸整个 run(traceNode 缺失条目被丢弃)", () => {
335
+ const run = makeRun("wf-shape-call");
336
+ const node = {
337
+ stepIndex: 0,
338
+ agent: "coder",
339
+ task: "do work",
340
+ model: "test-model",
341
+ status: "completed" as const,
342
+ };
343
+ run.state.trace.append(node);
344
+ run.state.calls.set(0, new AgentCall(0, { prompt: "do work" }, node));
345
+
346
+ const snap = JSON.parse(JSON.stringify(toRunSnapshot(run)));
347
+ snap.state.calls.push({ notACall: true });
348
+
349
+ const back = fromRunSnapshot(snap)!;
350
+ expect(back).toBeDefined();
351
+ expect(back.state.calls.size).toBe(1);
352
+ });
353
+ });