@zhushanwen/pi-subagent-workflow 0.1.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 (143) hide show
  1. package/agents/context-builder.md +17 -0
  2. package/agents/general-purpose.md +16 -0
  3. package/agents/oracle.md +17 -0
  4. package/agents/planner.md +17 -0
  5. package/agents/researcher.md +17 -0
  6. package/agents/reviewer.md +17 -0
  7. package/agents/scout.md +17 -0
  8. package/agents/worker.md +16 -0
  9. package/examples/README.md +43 -0
  10. package/examples/chain.example.js +92 -0
  11. package/examples/map-reduce.example.js +99 -0
  12. package/examples/parallel.example.js +82 -0
  13. package/examples/scatter-gather.example.js +106 -0
  14. package/index.ts +1 -0
  15. package/package.json +66 -0
  16. package/skills/workflow-script-format/SKILL.md +328 -0
  17. package/src/execution/__tests__/agent-registry.test.ts +164 -0
  18. package/src/execution/__tests__/agent-result-mapper.test.ts +128 -0
  19. package/src/execution/__tests__/alive-store.test.ts +147 -0
  20. package/src/execution/__tests__/bg-notify-render.test.ts +256 -0
  21. package/src/execution/__tests__/concurrency-pool.test.ts +217 -0
  22. package/src/execution/__tests__/config.test.ts +110 -0
  23. package/src/execution/__tests__/crash-recovery.test.ts +311 -0
  24. package/src/execution/__tests__/execute-nesting.test.ts +359 -0
  25. package/src/execution/__tests__/execute-options-mapper.test.ts +138 -0
  26. package/src/execution/__tests__/execution-record.test.ts +959 -0
  27. package/src/execution/__tests__/finalized-marker.test.ts +82 -0
  28. package/src/execution/__tests__/format-schema-instruction.test.ts +135 -0
  29. package/src/execution/__tests__/format.test.ts +320 -0
  30. package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
  31. package/src/execution/__tests__/list-component.test.ts +347 -0
  32. package/src/execution/__tests__/model-resolver.test.ts +356 -0
  33. package/src/execution/__tests__/output-collector.test.ts +61 -0
  34. package/src/execution/__tests__/path-encoding.test.ts +75 -0
  35. package/src/execution/__tests__/pi-invocation.test.ts +73 -0
  36. package/src/execution/__tests__/record-store.test.ts +545 -0
  37. package/src/execution/__tests__/run-spawn-edges.test.ts +439 -0
  38. package/src/execution/__tests__/run-spawn-integration.test.ts +897 -0
  39. package/src/execution/__tests__/sdk-contract.test.ts +272 -0
  40. package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
  41. package/src/execution/__tests__/session-file-gc.test.ts +247 -0
  42. package/src/execution/__tests__/session-reconstructor.test.ts +359 -0
  43. package/src/execution/__tests__/session-runner-schema-env.test.ts +314 -0
  44. package/src/execution/__tests__/session-start-reaper.test.ts +227 -0
  45. package/src/execution/__tests__/spawn-args.test.ts +244 -0
  46. package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
  47. package/src/execution/__tests__/subagent-service.test.ts +678 -0
  48. package/src/execution/__tests__/subprocess-agent-runner.test.ts +389 -0
  49. package/src/execution/__tests__/temp-prompt.test.ts +53 -0
  50. package/src/execution/__tests__/timeout-integration.test.ts +381 -0
  51. package/src/execution/__tests__/tombstone-store.test.ts +73 -0
  52. package/src/execution/__tests__/tool-action.test.ts +330 -0
  53. package/src/execution/__tests__/turn-limiter.test.ts +65 -0
  54. package/src/execution/__tests__/worktree-manager.test.ts +423 -0
  55. package/src/execution/__tests__/worktree-registry.test.ts +161 -0
  56. package/src/execution/agent-registry.ts +252 -0
  57. package/src/execution/agent-result-mapper.ts +84 -0
  58. package/src/execution/alive-store.ts +92 -0
  59. package/src/execution/best-effort.ts +30 -0
  60. package/src/execution/concurrency-pool.ts +84 -0
  61. package/src/execution/config.ts +73 -0
  62. package/src/execution/execute-options-mapper.ts +86 -0
  63. package/src/execution/execution-record.ts +778 -0
  64. package/src/execution/finalized-marker.ts +51 -0
  65. package/src/execution/model-config-service.ts +225 -0
  66. package/src/execution/model-resolver.ts +247 -0
  67. package/src/execution/notifier.ts +168 -0
  68. package/src/execution/output-collector.ts +88 -0
  69. package/src/execution/path-encoding.ts +34 -0
  70. package/src/execution/pi-invocation.ts +70 -0
  71. package/src/execution/record-store.ts +350 -0
  72. package/src/execution/session-context-resolver.ts +64 -0
  73. package/src/execution/session-file-gc.ts +98 -0
  74. package/src/execution/session-reconstructor.ts +450 -0
  75. package/src/execution/session-runner.ts +725 -0
  76. package/src/execution/spawn-event-adapter.ts +150 -0
  77. package/src/execution/subagent-service.ts +973 -0
  78. package/src/execution/subprocess-agent-runner.ts +108 -0
  79. package/src/execution/temp-prompt.ts +57 -0
  80. package/src/execution/tombstone-store.ts +72 -0
  81. package/src/execution/turn-limiter.ts +88 -0
  82. package/src/execution/types.ts +634 -0
  83. package/src/execution/worktree-manager.ts +285 -0
  84. package/src/execution/worktree-registry.ts +144 -0
  85. package/src/index.ts +454 -0
  86. package/src/interface/bg-notify-render.ts +286 -0
  87. package/src/interface/commands.ts +157 -0
  88. package/src/interface/format.ts +501 -0
  89. package/src/interface/gui-adapter.ts +136 -0
  90. package/src/interface/helpers.ts +110 -0
  91. package/src/interface/list-component.ts +643 -0
  92. package/src/interface/list-shared.ts +84 -0
  93. package/src/interface/list-view.ts +373 -0
  94. package/src/interface/reentry-guard.ts +30 -0
  95. package/src/interface/subagent-actions.ts +294 -0
  96. package/src/interface/subagent-tool.ts +294 -0
  97. package/src/interface/subagents.ts +30 -0
  98. package/src/interface/tool-render.ts +333 -0
  99. package/src/interface/tool-workflow-script.ts +351 -0
  100. package/src/interface/tool-workflow.ts +485 -0
  101. package/src/interface/views/WorkflowsView.ts +944 -0
  102. package/src/interface/views/detail-content.ts +298 -0
  103. package/src/interface/views/format.ts +320 -0
  104. package/src/orchestration/__tests__/concurrency-gate.test.ts +125 -0
  105. package/src/orchestration/__tests__/config-loader.test.ts +381 -0
  106. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +332 -0
  107. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
  108. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +248 -0
  109. package/src/orchestration/__tests__/lifecycle.test.ts +385 -0
  110. package/src/orchestration/__tests__/script-lint.test.ts +347 -0
  111. package/src/orchestration/__tests__/worker-script-builder.test.ts +42 -0
  112. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +319 -0
  113. package/src/orchestration/agent-opts-resolver.ts +128 -0
  114. package/src/orchestration/concurrency-gate.ts +69 -0
  115. package/src/orchestration/config-loader.ts +313 -0
  116. package/src/orchestration/error-recovery.ts +578 -0
  117. package/src/orchestration/execute-agent-call.ts +174 -0
  118. package/src/orchestration/jsonl-run-store.ts +292 -0
  119. package/src/orchestration/launcher.ts +368 -0
  120. package/src/orchestration/lifecycle.ts +373 -0
  121. package/src/orchestration/models/__tests__/budget.test.ts +367 -0
  122. package/src/orchestration/models/agent-call.ts +76 -0
  123. package/src/orchestration/models/budget.ts +148 -0
  124. package/src/orchestration/models/ports.ts +165 -0
  125. package/src/orchestration/models/run-runtime.ts +91 -0
  126. package/src/orchestration/models/run-spec.ts +54 -0
  127. package/src/orchestration/models/run-state.ts +44 -0
  128. package/src/orchestration/models/trace.ts +102 -0
  129. package/src/orchestration/models/types.ts +242 -0
  130. package/src/orchestration/models/workflow-run.ts +275 -0
  131. package/src/orchestration/models/workflow-script-registry.ts +32 -0
  132. package/src/orchestration/models/workflow-script.ts +90 -0
  133. package/src/orchestration/node-ops.ts +192 -0
  134. package/src/orchestration/script-lint.ts +387 -0
  135. package/src/orchestration/skill-discovery.ts +60 -0
  136. package/src/orchestration/worker-handle.ts +115 -0
  137. package/src/orchestration/worker-host.ts +93 -0
  138. package/src/orchestration/worker-script-builder.ts +281 -0
  139. package/src/orchestration/workflow-files.ts +85 -0
  140. package/src/orchestration/workflow-script-registry-impl.ts +128 -0
  141. package/src/shared/__tests__/resource-discovery.test.ts +226 -0
  142. package/src/shared/agent-event.ts +13 -0
  143. package/src/shared/resource-discovery.ts +535 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Workflow Extension — Worker Handle
3
+ *
4
+ * node:worker_threads.Worker 的线程句柄封装。技术资源,Infra 层具体类(D-12)。
5
+ * RunRuntime 直接持有,不经 interface(§domain-models 9)。
6
+ *
7
+ * 核心职责:竞态防护(G-025)。
8
+ *
9
+ * 背景:一个 run 可经历多个 WorkerHandle(pause/resume/retry 各换一个)。
10
+ * 需防止「terminate(old) → start(new) → old exit fires」竞态——
11
+ * WorkerHandle 把守卫内化:terminate 后 isCurrent=false,
12
+ * 已终止 handle 的 onMessage/onError/onExit 回调自动 no-op(无需调用方比对引用)。
13
+ *
14
+ * 层归属:Infra(D-12)。仅依赖 node:worker_threads(Node 原生)。
15
+ */
16
+
17
+ import { type Worker } from "node:worker_threads";
18
+
19
+ // ── Handler signatures ───────────────────────────────────────
20
+
21
+ /** Worker → Main 业务消息回调。 */
22
+ export type WorkerMessageHandler = (raw: unknown) => void;
23
+ /** Worker 线程 uncaught error 回调。 */
24
+ export type WorkerErrorHandler = (err: Error) => void;
25
+ /** Worker 线程 exit 回调(code=0 正常退出,非 0 崩溃)。 */
26
+ export type WorkerExitHandler = (code: number) => void;
27
+
28
+ // ── WorkerHandle ──────────────────────────────────────────────
29
+
30
+ export class WorkerHandle {
31
+ private readonly worker: Worker;
32
+ /**
33
+ * 竞态守卫。true = 此 handle 仍是当前活动 handle;false = 已 terminate,
34
+ * 后续事件(已终止 worker 延迟触发的 message/error/exit)必须忽略。
35
+ *
36
+ * 终止后置 false 并永不回升(幂等语义)。新 handle 由调用方(WorkerHost)
37
+ * 重新创建,已终止 handle 留在内存里直到 GC,但其回调全部 no-op。
38
+ */
39
+ private current = true;
40
+
41
+ constructor(worker: Worker) {
42
+ this.worker = worker;
43
+ }
44
+
45
+ /** 此 handle 是否仍是当前活动 handle(terminate 后 false,G-025)。 */
46
+ get isCurrent(): boolean {
47
+ return this.current;
48
+ }
49
+
50
+ /** 底层 Worker(WorkerHost/RunRuntime 偶尔需要直接访问,如 ref/href)。 */
51
+ get raw(): Worker {
52
+ return this.worker;
53
+ }
54
+
55
+ /**
56
+ * 向 worker 发送消息。terminate 后 no-op(已终止 handle 的 postMessage 无意义)。
57
+ */
58
+ postMessage(msg: unknown): void {
59
+ if (!this.current) return;
60
+ this.worker.postMessage(msg);
61
+ }
62
+
63
+ /**
64
+ * 终止 worker 线程。幂等——重复调用安全,第二次起 no-op。
65
+ * 置 isCurrent=false 后再 await worker.terminate,确保并发 exit 事件
66
+ * 在 terminate resolve 之前到达时也被守卫拦下。
67
+ */
68
+ async terminate(): Promise<void> {
69
+ if (!this.current) return;
70
+ this.current = false;
71
+ try {
72
+ await this.worker.terminate();
73
+ } catch (err) {
74
+ // terminate 失败不阻断(worker 可能已退出)。current 已 false,安全。
75
+ // 不向上抛——调用方(RunRuntime.release)不应被底层线程错误打断。
76
+ void err;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * 绑定 message 回调。仅当 isCurrent 时触发——已终止 handle 的事件被吞掉。
82
+ * 返回 this 便于链式 onMessage(...).onError(...).onExit(...)。
83
+ */
84
+ onMessage(handler: WorkerMessageHandler): this {
85
+ this.worker.on("message", (raw: unknown) => {
86
+ if (!this.current) return;
87
+ handler(raw);
88
+ });
89
+ return this;
90
+ }
91
+
92
+ /**
93
+ * 绑定 error 回调。仅当 isCurrent 时触发。
94
+ */
95
+ onError(handler: WorkerErrorHandler): this {
96
+ this.worker.on("error", (err: Error) => {
97
+ if (!this.current) return;
98
+ handler(err);
99
+ });
100
+ return this;
101
+ }
102
+
103
+ /**
104
+ * 绑定 exit 回调。仅当 isCurrent 时触发——这是 G-025 的关键守卫:
105
+ * terminate(old) → startWorker(new) → old exit 触发时,old handle.current
106
+ * 已为 false,回调 no-op,不会误删 new worker。
107
+ */
108
+ onExit(handler: WorkerExitHandler): this {
109
+ this.worker.on("exit", (code: number) => {
110
+ if (!this.current) return;
111
+ handler(code);
112
+ });
113
+ return this;
114
+ }
115
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Workflow Extension — Worker Host
3
+ *
4
+ * WorkerHost port 的 Infra 实现。
5
+ *
6
+ * 职责:启动一个 Worker thread 运行 workflow 脚本,返回 WorkerHandle,
7
+ * 并把 worker 的 message/error/exit 事件绑定到调用方注入的 WorkerHandlers。
8
+ *
9
+ * 层归属:Infra(D-12)。implements Engine 层的 WorkerHost port。
10
+ *
11
+ * 设计:
12
+ * - WorkerHostImpl implements WorkerHost(而非散落的 free function)。
13
+ * - 返回 WorkerHandle(封装),而非裸 Worker——onExit 传 handle 给
14
+ * handlers.onExit(code, handle),调用方用 handle.isCurrent 做竞态防护(C.3 + G-025)。
15
+ * - eval:true + 内联 buildWorkerScript 源码字符串(C.2:不用不存在的 bootstrap 文件)。
16
+ * - workerData: { scriptPath, args, workspace, meta }(不含 callCache/budget——
17
+ * 这些是 RunState 字段,由 lifecycle 在调用 start 前注入到 args 或独立处理)。
18
+ * - temp file 清理逻辑移到 Engine lifecycle,本处不管。
19
+ */
20
+
21
+ import { Worker } from "node:worker_threads";
22
+
23
+ import type { WorkerHandlers, WorkerHost } from "./models/ports.ts";
24
+ import type { RunSpec } from "./models/run-spec.ts";
25
+ import { WorkerHandle } from "./worker-handle.ts";
26
+ import { buildWorkerScript } from "./worker-script-builder.ts";
27
+
28
+ // ── WorkerHostImpl ───────────────────────────────────────────
29
+
30
+ export class WorkerHostImpl implements WorkerHost {
31
+ /**
32
+ * 启动一个 Worker thread 运行 workflow 脚本。
33
+ *
34
+ * 1. 用 buildWorkerScript(spec.scriptSource) 包装用户脚本(注入 agent/parallel/
35
+ * pipeline/$ARGS/$BUDGET 等全局,AC-4 格式契约由 buildWorkerScript 保证)
36
+ * 2. new Worker(code, { eval: true, workerData })(C.2 修复:eval 内联源码,
37
+ * 不 require bootstrap 文件)
38
+ * 3. 包装为 WorkerHandle,绑定 onMessage/onError/onExit 回调到 handlers
39
+ * 4. onExit 传 handle 给 handlers.onExit(code, handle)(C.3 修复——调用方用
40
+ * handle.isCurrent 做竞态防护,G-025)
41
+ *
42
+ * 返回的 WorkerHandle 由调用方(lifecycle)保存到 RunRuntime.worker。
43
+ * 终止/pause/resume 时由 RunRuntime.release 接管。
44
+ */
45
+ start(
46
+ spec: RunSpec,
47
+ args: Record<string, unknown>,
48
+ handlers: WorkerHandlers,
49
+ ): WorkerHandle {
50
+ const workerCode = buildWorkerScript(spec.scriptSource);
51
+
52
+ const worker = new Worker(workerCode, {
53
+ eval: true,
54
+ workerData: {
55
+ scriptPath: spec.scriptPath,
56
+ args,
57
+ workspace: process.cwd(),
58
+ meta: {
59
+ name: spec.scriptName,
60
+ description: spec.description,
61
+ },
62
+ // D-12 regression fix (round-2 #1):注入 budget,否则 worker 内 $BUDGET.total 恒为 0。
63
+ // 旧 agent-call-handler.ts 删除时同时丢失了 budget 注入和 budget-update 发送方,
64
+ // 导致依赖 $BUDGET 做动态预算分支的脚本静默得到全 0。
65
+ budget: {
66
+ maxTokens: spec.budgetTokens,
67
+ usedTokens: 0,
68
+ usedCost: 0,
69
+ },
70
+ },
71
+ });
72
+
73
+ const handle = new WorkerHandle(worker);
74
+
75
+ // 绑定事件——WorkerHandle 内部用 isCurrent 守卫,terminate 后回调 no-op(G-025)。
76
+ // handlers 的 onMessage/onError/onExit 都是 async,这里 void 掉 promise(worker 事件
77
+ // 不能 await,且 handler 内部错误由 lifecycle 统一捕获)。
78
+ handle.onMessage((raw) => {
79
+ void handlers.onMessage(raw);
80
+ });
81
+ handle.onError((err) => {
82
+ void handlers.onError(err);
83
+ });
84
+ handle.onExit((code) => {
85
+ // C.3 修复:传 handle 给 onExit,调用方用 handle.isCurrent 做竞态防护。
86
+ // 注意:此时 handle.isCurrent 仍为 true(onExit 回调仅在 isCurrent 时触发,
87
+ // WorkerHandle 的内部守卫已过滤掉 terminate 后的 stale exit)。
88
+ void handlers.onExit(code, handle);
89
+ });
90
+
91
+ return handle;
92
+ }
93
+ }
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Workflow Extension — Worker Script Builder
3
+ *
4
+ * 生成运行 workflow 脚本的 Worker 线程源码字符串:注入全局函数
5
+ * agent/parallel/pipeline/phase/log,并在 worker 内部处理
6
+ * parentPort 消息循环(agent-call / agent-result / abort / budget-update)。
7
+ *
8
+ * 层归属:Infra(源码字符串生成,纯文本拼接,无 Pi 依赖)。
9
+ *
10
+ * 设计:
11
+ * - WorkerLogEntry 类型来源 engine/models/types.js(不在本文件重复声明)。
12
+ * - **AC-4 不变式**:buildWorkerScript 生成的脚本格式逐字保留——
13
+ * 用户资产(workflow 脚本依赖 agent/parallel/pipeline/$ARGS/$BUDGET 等契约)。
14
+ *
15
+ * 兼容 Claude Code Workflow 脚本格式:
16
+ * - agent(promptString) / agent(promptString, { label?, schema?, model?, scene? }) /
17
+ * agent({ prompt, schema?, model?, scene?, description? })
18
+ * - parallel([agent(...), ...]) 或 parallel([{ task, agent }, ...])
19
+ * - pipeline([stageFn, ...])
20
+ * - phase(name), log(msg)
21
+ * - $ARGS, $WORKSPACE, $BUDGET
22
+ * - module.exports = { meta, execute } 自动调用
23
+ *
24
+ * 生成的源码通过 `new Worker(code, { eval: true, workerData })` 在隔离的 Worker 线程运行。
25
+ *
26
+ * 通信协议(AC-4 契约,逐字保留):
27
+ * Worker → Main (postMessage):
28
+ * { type: "agent-call", callId: number, opts: AgentCallOpts }
29
+ * { type: "workflow-call", callId: number, name: string, args: Record<string, unknown> }
30
+ * { type: "return", runId: string, result: unknown }
31
+ * { type: "error", runId: string, error: string }
32
+ * { type: "log", phase: string, message: string }
33
+ *
34
+ * Main → Worker (parentPort.on("message")):
35
+ * { type: "agent-result", callId: number, result: AgentResult, cached: boolean }
36
+ * { type: "workflow-result", callId: number, result: unknown }
37
+ * { type: "budget-update", budget: unknown }
38
+ * { type: "abort", reason: string }
39
+ */
40
+
41
+ // ── Build worker source ─────────────────────────────────────
42
+
43
+ /**
44
+ * Build the complete worker source text by wrapping the user's workflow script
45
+ * with infrastructure code and injected global functions.
46
+ *
47
+ * AC-4:脚本格式不变(用户资产)。逐字保留旧 buildWorkerScript 的拼接逻辑。
48
+ */
49
+ export function buildWorkerScript(userScript: string): string {
50
+ return [
51
+ '"use strict";',
52
+ '// Module-scope: accessible to the outer .catch() for surfacing logs on errors.',
53
+ 'const _workerLogs = [];',
54
+ 'function _pushWorkerLog(level, args) {',
55
+ ' try { _workerLogs.push({ level, message: args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ") }); } catch (e) { /* swallow */ }',
56
+ '}',
57
+ '(async () => {',
58
+ ' const { parentPort, workerData } = require("node:worker_threads");',
59
+ '',
60
+ ' if (!parentPort) {',
61
+ ' throw new Error("Workflow worker: parentPort is null — not running in a Worker thread");',
62
+ ' }',
63
+ '',
64
+ ' // ── Intercept console.* to avoid leaking worker diagnostics into the input area ──',
65
+ ' // _workerLogs + _pushWorkerLog are declared at module scope (above the IIFE)',
66
+ ' // so the outer .catch() can include them on script errors.',
67
+ ' console.log = function (...args) { _pushWorkerLog("log", args); };',
68
+ ' console.warn = function (...args) { _pushWorkerLog("warn", args); };',
69
+ ' console.error = function (...args) { _pushWorkerLog("error", args); };',
70
+ ' console.info = function (...args) { _pushWorkerLog("info", args); };',
71
+ '',
72
+ ' // ── Internal state ──',
73
+ ' let _callIdCounter = 0;',
74
+ ' let _agentCallCount = 0;',
75
+ ' const _pendingCalls = new Map();',
76
+ ' const _callCache = workerData.callCache instanceof Map',
77
+ ' ? workerData.callCache',
78
+ ' : new Map(Object.entries(workerData.callCache || {}).map(([k, v]) => [Number(k), v]));',
79
+ '',
80
+ ' // ── Injected globals ──',
81
+ ' const $ARGS = (workerData.args && typeof workerData.args === "object") ? workerData.args : {};\n' +
82
+ ' const args = $ARGS;',
83
+ ' const $WORKSPACE = typeof workerData.workspace === "string" ? workerData.workspace : "";',
84
+ ' const _budgetData = {',
85
+ ' total: (workerData.budget && workerData.budget.maxTokens) || 0,',
86
+ ' _spentTokens: (workerData.budget && workerData.budget.usedTokens) ?? 0,',
87
+ ' _spentCost: (workerData.budget && workerData.budget.usedCost) ?? 0,',
88
+ ' };',
89
+ ' const $BUDGET = {',
90
+ ' get total() { return _budgetData.total; },',
91
+ ' spent() { return _budgetData._spentTokens; },',
92
+ ' remaining() { return Math.max(0, _budgetData.total - _budgetData._spentTokens); },',
93
+ ' };',
94
+ '',
95
+ ' // ── WorkflowAbortedError ──',
96
+ ' class WorkflowAbortedError extends Error {',
97
+ ' constructor(reason) {',
98
+ ' super("Workflow aborted: " + (reason || "No reason"));',
99
+ ' this.name = "WorkflowAbortedError";',
100
+ ' this.reason = reason || "";',
101
+ ' }',
102
+ ' }',
103
+ '',
104
+ ' // ── Message handler (main thread → worker) ──',
105
+ ' parentPort.on("message", (msg) => {',
106
+ ' if (msg.type === "agent-result") {',
107
+ ' const pending = _pendingCalls.get(msg.callId);',
108
+ ' if (pending) {',
109
+ ' _pendingCalls.delete(msg.callId);',
110
+ ' if (typeof msg.result !== "undefined") {',
111
+ ' _callCache.set(msg.callId, msg.result);',
112
+ ' }',
113
+ ' // 失败不传播到 agent 外部:resolve 而非 reject。',
114
+ ' // 旧实现在 result.error 时 reject,单 agent 失败会冒到 worker 顶层 .catch()',
115
+ ' // → 发 type:"error" → handleScriptError → rebuildRuntime → SIGKILL 同伴进程,',
116
+ ' // 把单点失败放大成整批崩溃。改为始终 resolve(错误时回退到 content 文本),',
117
+ ' // 让 parallel() 下的脚本容错循环(parseResult → null → skip)自然接管。',
118
+ ' // 错误原因已由主线程 executeAgentCall → trace.update(result.error) 保留在 trace/TUI,',
119
+ ' // 不丢失。skipNode 的 SKIP_PLACEHOLDER(无 error,resolve 为 "")已确立此先例。',
120
+ ' // parsedOutput: validated data object from structured-output execute().',
121
+ ' // Fallback to content (raw text) when no schema was requested or on error.',
122
+ ' pending.resolve(msg.result.parsedOutput ?? msg.result.content);',
123
+ ' }',
124
+ ' } else if (msg.type === "workflow-result") {',
125
+ ' const pending = _pendingCalls.get(msg.callId);',
126
+ ' if (pending) {',
127
+ ' _pendingCalls.delete(msg.callId);',
128
+ ' pending.resolve(msg.result);',
129
+ ' }',
130
+ ' } else if (msg.type === "abort") {',
131
+ ' const err = new WorkflowAbortedError(msg.reason);',
132
+ ' _pendingCalls.forEach((p) => { p.reject(err); });',
133
+ ' _pendingCalls.clear();',
134
+ ' } else if (msg.type === "budget-update" && msg.budget) {',
135
+ ' _budgetData._spentTokens = msg.budget.usedTokens ?? _budgetData._spentTokens;',
136
+ ' _budgetData._spentCost = msg.budget.usedCost ?? _budgetData._spentCost;',
137
+ ' }',
138
+ ' // "budget-warning" is informational; no required handling',
139
+ ' });',
140
+ '',
141
+ // ── phase global ──
142
+ ' let _currentPhase = "";',
143
+ ' function phase(name) { _currentPhase = String(name); }',
144
+ '',
145
+ // ── log global ──
146
+ ' function log(msg) {',
147
+ ' try { parentPort.postMessage({ type: "log", phase: _currentPhase, message: String(msg) }); } catch(e) { /* swallow */ }',
148
+ ' }',
149
+ '',
150
+ // ── agent global — CC-compatible multi-signature ──
151
+ ' async function agent(firstArg, secondArg) {',
152
+ ' let opts;',
153
+ ' if (typeof firstArg === "string") {',
154
+ ' opts = {',
155
+ ' prompt: firstArg,',
156
+ ' description: (secondArg && typeof secondArg === "object" && secondArg.label) || undefined,',
157
+ ' schema: (secondArg && typeof secondArg === "object" && secondArg.schema) || undefined,',
158
+ ' model: (secondArg && typeof secondArg === "object" && secondArg.model) || undefined,',
159
+ ' scene: (secondArg && typeof secondArg === "object" && secondArg.scene) || undefined,\n' +
160
+ ' phase: (secondArg && typeof secondArg === "object" && secondArg.phase) || undefined,',
161
+ ' };',
162
+ ' } else if (typeof firstArg === "object" && firstArg !== null) {',
163
+ ' if (firstArg.prompt) {',
164
+ ' opts = firstArg;',
165
+ ' } else if (firstArg.task || firstArg.agent) {',
166
+ ' opts = {',
167
+ ' prompt: firstArg.task || firstArg.prompt || "",',
168
+ ' description: firstArg.label || firstArg.description,',
169
+ ' agent: firstArg.agent,',
170
+ ' schema: firstArg.schema,',
171
+ ' model: firstArg.model,',
172
+ ' scene: firstArg.scene,',
173
+ ' timeoutMs: firstArg.timeoutMs,',
174
+ ' cwd: firstArg.cwd,',
175
+ ' };',
176
+ ' } else {',
177
+ ' opts = firstArg;',
178
+ ' }',
179
+ ' } else {',
180
+ ' throw new Error("agent() requires a prompt string or options object as first argument");',
181
+ ' }',
182
+ '',
183
+ ' // Validate known agent() fields to catch API misuse early',
184
+ ' const _knownFields = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "cwd"]);',
185
+ ' const _unknownFields = Object.keys(opts).filter((k) => !_knownFields.has(k));',
186
+ ' if (_unknownFields.length > 0) {',
187
+ ' _pushWorkerLog("warn", ["[workflow] agent() received unknown fields: " + _unknownFields.join(", ") + ". Known fields: prompt, description, schema, model, scene, label, task, agent, phase, skill, timeoutMs, cwd"]);',
188
+ ' }',
189
+ '',
190
+ ' const callId = _callIdCounter;',
191
+ ' _callIdCounter++;',
192
+ ' _agentCallCount++;',
193
+ ' if (_callCache.has(callId)) {',
194
+ ' const cached = _callCache.get(callId);',
195
+ ' // 与 live handler 对齐:失败也 resolve(回退 content),不 throw。',
196
+ ' // 见 agent-result 消息处理的注释:拒绝传播失败到 agent 外部。',
197
+ ' return cached ? (cached.parsedOutput ?? cached.content) : undefined;',
198
+ ' }',
199
+ '',
200
+ ' const _effectivePhase = opts.phase || _currentPhase;\n' +
201
+ ' delete opts.phase;\n' +
202
+ '\n' +
203
+ ' parentPort.postMessage({ type: "agent-call", callId, opts, phase: _effectivePhase });',
204
+ ' return new Promise((resolve, reject) => {',
205
+ ' _pendingCalls.set(callId, { resolve, reject });',
206
+ ' });',
207
+ ' }',
208
+ '',
209
+ // ── parallel global — CC-compatible ──
210
+ // allSettled 语义:单个 agent 的意外 reject(postMessage 失败等基础设施异常、abort)
211
+ // 不拖垮整批。rejected 结果降级为错误消息字符串(与 agent() 的 error→content 回退一致,
212
+ // parseResult(string) → null → 脚本 soft-fail)。B1 之后 agent() 不再因 agent 失败 reject,
213
+ // 这里作为纵深防御保留。
214
+ ' async function parallel(calls) {',
215
+ ' if (typeof calls === "function") { return calls(); }',
216
+ ' const settled = await Promise.allSettled(calls.map((c) => {',
217
+ ' if (typeof c === "function") { return c(); }',
218
+ ' if (typeof c === "object" && c !== null && (c.task || c.agent)) { return agent(c); }',
219
+ ' return agent(c);',
220
+ ' }));',
221
+ ' return settled.map((r) => r.status === "fulfilled" ? r.value : (r.reason instanceof Error ? r.reason.message : String(r.reason)));',
222
+ ' }',
223
+ '',
224
+ // ── pipeline global ──
225
+ ' async function pipeline(firstArg, ...restStages) {',
226
+ ' // Single-arg mode: pipeline([stage1, stage2, ...])',
227
+ ' if (Array.isArray(firstArg) && restStages.length === 0) {',
228
+ ' let result;',
229
+ ' for (const stage of firstArg) { result = await stage(result); }',
230
+ ' return result;',
231
+ ' }',
232
+ ' // Cartesian product mode: pipeline([items], stage1, stage2, ...)',
233
+ ' if (Array.isArray(firstArg) && restStages.length > 0 && typeof restStages[0] === "function") {',
234
+ ' const results = [];',
235
+ ' for (const item of firstArg) {',
236
+ ' let val = item;',
237
+ ' let failed = false;',
238
+ ' for (const stage of restStages) {',
239
+ ' if (failed) break;',
240
+ ' try { val = await stage(val); }',
241
+ ' catch (e) { val = null; failed = true; }',
242
+ ' }',
243
+ ' results.push(val);',
244
+ ' }',
245
+ ' return results;',
246
+ ' }',
247
+ ' throw new Error("pipeline() expects pipeline([stage1, ...]) or pipeline([items], stage1, ...)");',
248
+ ' }',
249
+ '',
250
+ ' // ── workflow global — nested workflow invocation ──',
251
+ ' async function workflow(name, args) {',
252
+ ' if (typeof name !== "string" || name.length === 0) {',
253
+ ' throw new Error("workflow() requires a workflow name string as first argument");',
254
+ ' }',
255
+ ' const workflowArgs = (typeof args === "object" && args !== null) ? args : {};',
256
+ ' const callId = _callIdCounter;',
257
+ ' _callIdCounter++;',
258
+ ' parentPort.postMessage({ type: "workflow-call", callId, name, args: workflowArgs });',
259
+ ' return new Promise((resolve, reject) => {',
260
+ ' _pendingCalls.set(callId, { resolve, reject });',
261
+ ' });',
262
+ ' }',
263
+ '',
264
+ ' // ── User workflow script ──',
265
+ ' ' + userScript,
266
+ '',
267
+ ' // ── Auto-invoke execute() for module.exports pattern ──',
268
+ ' if (typeof module !== "undefined" && module.exports && typeof module.exports.execute === "function") {',
269
+ ' return await module.exports.execute({ agent, parallel, pipeline, phase, log, workflow, $ARGS, $WORKSPACE, $BUDGET });',
270
+ ' }',
271
+ '})().then((result) => {',
272
+ ' const { parentPort, workerData } = require("node:worker_threads");',
273
+ ' const runId = (workerData.args && typeof workerData.args === "object" && workerData.args._runId) || "";',
274
+ ' parentPort.postMessage({ type: "return", runId, result, workerLogs: _workerLogs });',
275
+ '}).catch((err) => {',
276
+ ' const { parentPort, workerData } = require("node:worker_threads");',
277
+ ' const runId = (workerData.args && typeof workerData.args === "object" && workerData.args._runId) || "";',
278
+ ' parentPort.postMessage({ type: "error", runId, error: err.message || String(err), workerLogs: _workerLogs });',
279
+ '});',
280
+ ].join("\n");
281
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Workflow 文件持久化操作(save / delete)。
3
+ *
4
+ * 历史:saveWorkflow 曾有两套实现——commands.ts 用 renameSync 仅 project scope,
5
+ * WorkflowsView.ts 用 copyFileSync 支持 user scope。本次统一为 rename + 仅 project
6
+ * scope(决策 2):tmp 文件保存后自动消失,保存位置固定 .pi/workflows/。
7
+ *
8
+ * 代价:TUI 失去 user scope Tab 切换(功能倒退,已接受);
9
+ * Windows/跨设备 rename 可能失败(已知风险,接受)。
10
+ */
11
+
12
+ import { existsSync, mkdirSync, renameSync, unlinkSync } from "node:fs";
13
+ import { resolve } from "node:path";
14
+
15
+ // ── Path helpers (computed at call time to respect cwd changes in tests) ──
16
+
17
+ function getTmpDir(): string {
18
+ return resolve(".pi/workflows/.tmp");
19
+ }
20
+
21
+ function getSavedDir(): string {
22
+ return resolve(".pi/workflows");
23
+ }
24
+
25
+ // ── Save ──────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * 保存临时 workflow:.pi/workflows/.tmp/{name}.js → .pi/workflows/{newName||name}.js
29
+ * 用 rename(tmp 文件保存后消失)。仅 project scope。
30
+ *
31
+ * 直接按路径查找 tmp 文件,不调 config-loader 全扫——save 只需知道 tmp 文件
32
+ * 的路径,不需要 meta 提取或跨目录去重。
33
+ *
34
+ * @throws 若 tmp workflow 不存在、目标已存在、或 rename 失败
35
+ */
36
+ export async function saveWorkflow(tmpName: string, newName?: string): Promise<string> {
37
+ const srcPath = resolve(getTmpDir(), `${tmpName}.js`);
38
+ if (!existsSync(srcPath)) {
39
+ throw new Error(`Temporary workflow '${tmpName}' not found`);
40
+ }
41
+
42
+ const destName = newName ?? tmpName;
43
+ const savedDir = getSavedDir();
44
+ const destPath = resolve(savedDir, `${destName}.js`);
45
+
46
+ if (existsSync(destPath)) {
47
+ throw new Error(`'${destName}' already exists in saved workflows. Use a different name.`);
48
+ }
49
+
50
+ mkdirSync(savedDir, { recursive: true });
51
+ renameSync(srcPath, destPath);
52
+ return `Saved '${tmpName}' → '${destName}' (${destPath})`;
53
+ }
54
+
55
+ // ── Delete ────────────────────────────────────────────────────
56
+
57
+ /**
58
+ * 删除 workflow 脚本文件(tmp 或 saved)。
59
+ * @param isRunning 回调,判断某 name 是否正在运行(运行中拒绝删除)
60
+ * @throws 若正在运行、或文件不存在
61
+ */
62
+ export function deleteWorkflow(
63
+ name: string,
64
+ isRunning: (name: string) => boolean,
65
+ ): string {
66
+ if (isRunning(name)) {
67
+ throw new Error(`Cannot delete '${name}': workflow is currently running. Abort it first.`);
68
+ }
69
+
70
+ const tmpDir = getTmpDir();
71
+ const savedDir = getSavedDir();
72
+ const candidates = [
73
+ resolve(tmpDir, `${name}.js`),
74
+ resolve(savedDir, `${name}.js`),
75
+ ];
76
+
77
+ for (const filePath of candidates) {
78
+ if (existsSync(filePath)) {
79
+ unlinkSync(filePath);
80
+ return `Deleted workflow '${name}' (${filePath})`;
81
+ }
82
+ }
83
+
84
+ throw new Error(`Workflow file '${name}' not found`);
85
+ }