@zhushanwen/pi-subagent-workflow 8.14.7 → 9.0.1

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 (58) hide show
  1. package/README.md +6 -5
  2. package/package.json +14 -9
  3. package/relay/relay.mjs +45 -2
  4. package/skills/workflow-script-format/SKILL.md +2 -56
  5. package/src/host/__tests__/inflight-reporter.test.ts +26 -1
  6. package/src/host/__tests__/pi-host.test.ts +418 -4
  7. package/src/host/inflight-reporter.ts +13 -10
  8. package/src/host/pi-host.ts +205 -17
  9. package/src/index.ts +21 -72
  10. package/src/injectors/__tests__/engine-awareness.test.ts +236 -33
  11. package/src/injectors/__tests__/helpers/injector-test-mocks.ts +15 -7
  12. package/src/injectors/__tests__/workflow-list-injector.test.ts +447 -40
  13. package/src/injectors/resource-list-injector.ts +103 -32
  14. package/src/injectors/subagent-list-injector.ts +4 -28
  15. package/src/injectors/workflow-list-injector.ts +8 -4
  16. package/src/interface/__tests__/capture-tool.ts +56 -0
  17. package/src/interface/__tests__/commands-resume.test.ts +103 -0
  18. package/src/interface/__tests__/tool-prompt-contract.test.ts +361 -0
  19. package/src/interface/__tests__/tool-render.test.ts +105 -2
  20. package/src/interface/__tests__/tool-subagents.test.ts +24 -26
  21. package/src/interface/__tests__/tool-workflow-resume.test.ts +227 -0
  22. package/src/interface/__tests__/tool-workflow-script.test.ts +557 -0
  23. package/src/interface/__tests__/tool-workflow.test.ts +884 -0
  24. package/src/interface/bg-notify-render.ts +18 -9
  25. package/src/interface/command-actions.ts +22 -17
  26. package/src/interface/commands.ts +58 -12
  27. package/src/interface/format.ts +50 -16
  28. package/src/interface/id-preview.ts +4 -4
  29. package/src/interface/subagent-actions.ts +18 -50
  30. package/src/interface/subagent-tool.ts +26 -18
  31. package/src/interface/subagents.ts +5 -11
  32. package/src/interface/tool-render.ts +14 -30
  33. package/src/interface/tool-shared.ts +55 -12
  34. package/src/interface/tool-subagents.ts +21 -11
  35. package/src/interface/tool-workflow-script.ts +52 -72
  36. package/src/interface/tool-workflow.ts +345 -139
  37. package/src/interface/views/WorkflowsView.ts +10 -7
  38. package/src/interface/views/__tests__/{WorkflowsView-signature.test.ts → WorkflowsView.test.ts} +155 -11
  39. package/src/interface/views/detail-content.ts +3 -5
  40. package/src/jsonl-run-store.ts +656 -544
  41. package/src/model-events.ts +35 -0
  42. package/src/session-lifecycle.ts +349 -86
  43. package/src/subagents-events.ts +56 -0
  44. package/src/workflow-events.ts +285 -200
  45. package/src/workflow-notify.ts +456 -0
  46. package/src/injectors/__tests__/engine-section-stability.test.ts +0 -353
  47. package/src/injectors/__tests__/model-list-injector.test.ts +0 -136
  48. package/src/injectors/__tests__/subagent-list-injector.test.ts +0 -376
  49. package/src/interface/__tests__/detectors.test.ts +0 -161
  50. package/src/interface/__tests__/subagent-tool-prompt.test.ts +0 -183
  51. package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +0 -219
  52. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +0 -251
  53. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +0 -215
  54. package/src/interface/__tests__/workflow-state-file-exposure.test.ts +0 -38
  55. package/src/interface/__tests__/workflow-tool-prompt.test.ts +0 -129
  56. package/src/interface/helpers.ts +0 -272
  57. package/src/interface/views/__tests__/detail-content-session-file.test.ts +0 -73
  58. package/src/interface/views/__tests__/record-progress.test.ts +0 -235
package/README.md CHANGED
@@ -59,13 +59,14 @@ pi --tools todo,goal_control,workflow,subagent,ask_user
59
59
 
60
60
  系统内置 `n = 10` 深度护栏(`MAX_FORK_DEPTH`,fork 链与通用嵌套两个计数器共享上限、取严者生效):fork 链超限抛 `ForkDepthExceededError`,通用嵌套护栏报 `nested_spawn_rejected`。实测建议控制在 **3-4 层**以内——更深层会因上下文逐层压缩导致信息失真。
61
61
 
62
- ## Workflow 生命周期(one-shot)
62
+ ## Workflow 生命周期
63
63
 
64
- Workflow run 是一次性执行,状态机两态:`running → done`(`done` 唯一终态,reason 区分 completed / aborted / failed / budget_limited / time_limited)。`workflow` tool 仅 3 个 action:`run` / `status` / `abort`。run 的 `name` 接受 `<available_workflows>` 列出的 workflow 名(内置 chain / parallel / map-reduce / scatter-gather / review-fix-loop 或已保存脚本)或 `.js` 绝对路径。一次性执行、无 pause/resume:提前停止只有 abort 一条路,要新结果就重新 run。
64
+ Workflow run 有两类结束形态:**终局**(`done`,outcome 区分 done / failed / cancelled / time_limited——成败已定)与**中断**(`interrupted` 暂停态——执行被打断但可续跑)。`workflow` tool 共 4 个 action:`run` / `status` / `abort` / `resume`。run 的 `name` 接受 `<available_workflows>` 列出的 workflow 名(内置 chain / parallel / map-reduce / scatter-gather / review-fix-loop 或已保存脚本)或 `.js` 绝对路径。
65
65
 
66
- - **abort 是唯一的提前停止方式**:`{"action":"abort","runId":"<id>"}`(可选 `"error":"<reason>"`)。不存在 pause/resume action,调用会被 pi schema 校验拒绝(`Validation failed for tool "workflow"`);`/workflows pause|resume <id>` 返回 removed 提示
67
- - **session 切换/关闭时**,所有 running run 当即作废转 `done,failed`(state.error 为 `Session switched: run terminated` / `Session shutdown: run terminated`),已投入的 token 作废;需要结果就重新 run
68
- - **快照格式 `wf-run-v2`**(status 两态、无 `pausedAt`);旧 `wf-run-v1` 文件加载时静默跳过
66
+ - **abort(主动取消)**:`{"action":"abort","runId":"<id>"}`(可选 `"error":"<reason>"`)→ `cancelled` 终局。要新结果就重新 run
67
+ - **resume(断点续跑)**:`{"action":"resume","runId":"<id>"}`(可选 args 校验一致性与时间预算)——对 interrupted run 从断点恢复:已完成的调用回放缓存零 token、未完成的按三档恢复(会话完整 → 捞回结果;请求未完成 → 同会话续写;会话不在 → 整跑);`/workflows resume <id>` 同能力
68
+ - **session 切换/关闭时**,所有 running run 转 `interrupted` 暂停态(被打断不记失败)——回原 session 可 resume 续跑
69
+ - **持久化 = record 事件流**(`<runId>.record.jsonl`,append-only 单一事实源):状态由事件流折叠得出;旧 `wf-run-v1`/`wf-run-v2` 快照文件为历史遗留,读取面不消费
69
70
  - **worker 崩溃自动重建重试**(默认 3 次):重建时在飞 agent 调用被清除重跑,已完成的调用保留 replay 缓存,不重复消耗 token
70
71
 
71
72
  ## 性能:sessions-index.json 持久化索引
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-subagent-workflow",
3
- "version": "8.14.7",
3
+ "version": "9.0.1",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
@@ -25,7 +25,10 @@
25
25
  "src/index.ts",
26
26
  "src/jsonl-run-store.ts",
27
27
  "src/session-lifecycle.ts",
28
+ "src/model-events.ts",
29
+ "src/subagents-events.ts",
28
30
  "src/workflow-events.ts",
31
+ "src/workflow-notify.ts",
29
32
  "src/host/",
30
33
  "src/injectors/",
31
34
  "src/interface/"
@@ -55,20 +58,21 @@
55
58
  ]
56
59
  },
57
60
  "dependencies": {
58
- "@zhushanwen/extension-protocol": "0.14.0",
59
- "@zhushanwen/session-delivery": "0.10.1",
60
- "@zhushanwen/pi-ext-guards": "0.4.1",
61
+ "@zhushanwen/extension-protocol": "0.16.0",
62
+ "@zhushanwen/session-delivery": "0.11.0",
63
+ "@zhushanwen/pi-ext-guards": "0.4.2",
61
64
  "@zhushanwen/pi-extension-logger": "0.6.1",
62
- "@zhushanwen/subagent-core": "0.11.1",
63
- "@zhushanwen/pi-subagent-cli": "0.4.3",
64
- "@zhushanwen/zcode-subagent-cli": "0.4.1"
65
+ "@zhushanwen/pi-notify-ledger-host": "0.2.0",
66
+ "@zhushanwen/subagent-core": "0.13.0",
67
+ "@zhushanwen/zcode-subagent-cli": "0.4.3",
68
+ "@zhushanwen/pi-subagent-cli": "0.4.5"
65
69
  },
66
70
  "peerDependencies": {
67
71
  "@earendil-works/pi-ai": "^0.84.4",
68
72
  "@earendil-works/pi-coding-agent": "^0.84.4",
69
73
  "@earendil-works/pi-tui": "^0.84.4",
70
74
  "typebox": "*",
71
- "@zhushanwen/pi-pending-notifications": "0.7.7",
75
+ "@zhushanwen/pi-pending-notifications": "0.8.0",
72
76
  "@zhushanwen/pi-structured-output": "5.1.9"
73
77
  },
74
78
  "peerDependenciesMeta": {
@@ -82,7 +86,8 @@
82
86
  "devDependencies": {
83
87
  "@types/node": "^24.0.0",
84
88
  "@vitest/coverage-v8": "^4.1.9",
85
- "vitest": "^4.1.8"
89
+ "vitest": "^4.1.8",
90
+ "@zhushanwen/subagent-engine-sdk": "0.7.1"
86
91
  },
87
92
  "scripts": {
88
93
  "typecheck": "npx tsc --noEmit",
package/relay/relay.mjs CHANGED
@@ -33,6 +33,9 @@
33
33
  * dir="down"(代理 stdin → runtime → 真实 pi stdin)/ "up"(→ 代理 stdout)/
34
34
  * "up-stderr"(→ 代理 stderr)。方向以「数据流向真实 pi」为参照:下行 = 流向 pi。
35
35
  * - 退出帧 {"kind":"exit","code":C,"signal":S} → 代理以相同 code/signal 退出
36
+ * - goodbye 帧 {"v":1,"kind":"goodbye"}(代理 → runtime,收到宿主 SIGTERM/SIGINT 时):
37
+ * 正常收割预告——runtime 据此把后续 socket close 判定为正常 teardown(kill 行为不变,
38
+ * 仅日志分级),无预告的 close 保持异常断连语义(kill-on-disconnect warn)
36
39
  * - socket 断(close/error/EOF)→ 退出码 12:socket 是代理的生命线,断 = runtime
37
40
  * 崩溃等场景下 extension 侧感知「子进程死亡」的机制(崩溃矩阵②),不自杀会变孤儿。
38
41
  *
@@ -59,10 +62,14 @@ const RELAY_EXIT_CODES = {
59
62
  MISSING_IDENTITY: 13,
60
63
  };
61
64
 
62
- /** 首次连接失败到重试的间隔(毫秒)。设计未定值;取短值——只需覆盖 runtime 建 socket 与主 pi spawn 的毫秒级窗口。 */
63
- const CONNECT_RETRY_DELAY_MS = 200;
65
+ /** 首次连接失败到重试的间隔(毫秒)。设计未定值;取短值——只需覆盖 runtime 建 socket 与主 pi spawn 的毫秒级窗口。
66
+ * RELAY_CONNECT_RETRY_DELAY_MS 仅测试注入通道(集成测试注入短值压缩重试等待);NaN/非正数退回默认(负值会让 setTimeout 立即触发 busy retry)。 */
67
+ const _retryDelayEnv = Number(process.env.RELAY_CONNECT_RETRY_DELAY_MS);
68
+ const CONNECT_RETRY_DELAY_MS = Number.isFinite(_retryDelayEnv) && _retryDelayEnv > 0 ? _retryDelayEnv : 200;
64
69
  /** 退出前等待 stderr/stdout flush 的兜底上限(毫秒):pipe 写是异步的,process.exit 会丢未 flush 数据。 */
65
70
  const FLUSH_TIMEOUT_MS = 1000;
71
+ /** goodbye 帧写出后等待 flush 的兜底上限(毫秒):runtime 已死等形态下 write 回调永不到达。 */
72
+ const GOODBYE_FLUSH_TIMEOUT_MS = 500;
66
73
 
67
74
  const stdin = process.stdin;
68
75
  const stdout = process.stdout;
@@ -87,6 +94,42 @@ process.on("uncaughtException", (err) => {
87
94
  );
88
95
  });
89
96
 
97
+ // 宿主终止预告(goodbye 帧):宿主(extension/引擎)收割代理发 SIGTERM/SIGINT——
98
+ // 这通常发生在 agent_settled 后的正常收割,与宿主崩溃在 runtime 侧同表现为 socket
99
+ // close。被终止前先向 runtime 发 goodbye 预告帧,runtime 据此把 close 判定为正常
100
+ // teardown(info 级日志),异常断连才保留 warn 级 kill-on-disconnect。注册 handler 后
101
+ // Node 不再执行信号默认终止,发帧(含 flush 等待)后自行按 128+signo 退出,对宿主侧
102
+ // 的退出码口径(spawn close 折算 143)保持一致。
103
+ process.on("SIGTERM", () => hostTerminate("SIGTERM"));
104
+ process.on("SIGINT", () => hostTerminate("SIGINT"));
105
+
106
+ /** 宿主信号终止:best-effort 发 goodbye → 等 flush(有界)→ 按 128+signo 退出。 */
107
+ function hostTerminate(sig) {
108
+ if (exiting) return;
109
+ exiting = true;
110
+ const signum = os.constants.signals[sig];
111
+ const exitCode = 128 + (typeof signum === "number" ? signum : 0);
112
+ const die = () => process.exit(exitCode);
113
+ try {
114
+ if (sock !== null && !sock.destroyed && handshakeAccepted) {
115
+ // write 回调 = 帧已离开写缓冲;兜底超时防 runtime 已死时回调永不到达。
116
+ const timer = setTimeout(die, GOODBYE_FLUSH_TIMEOUT_MS);
117
+ timer.unref();
118
+ sock.write(
119
+ JSON.stringify({ v: RELAY_PROTOCOL_VERSION, kind: "goodbye" }) + "\n",
120
+ () => {
121
+ clearTimeout(timer);
122
+ die();
123
+ },
124
+ );
125
+ return;
126
+ }
127
+ } catch {
128
+ // socket 已坏(runtime 先死等形态):无预告通道,直接按信号口径退出
129
+ }
130
+ die();
131
+ }
132
+
90
133
  main();
91
134
 
92
135
  function main() {
@@ -151,11 +151,11 @@ const [r1, r2, r3] = await parallel([
151
151
  ]);
152
152
  ```
153
153
 
154
- 并发默认上限 6(ConcurrencyPool 限流,`maxConcurrent=6` 来源 ADR-030 决策 3),超出自动排队。元素也可以是返回 Promise 的函数,会被直接调用:
154
+ 并发默认上限 6(ConcurrencyPool 限流,`maxConcurrent=6` 来源 ADR-030 决策 3),超出自动排队。元素也可以是返回 Promise 的函数,会被直接调用。
155
155
 
156
156
  ### `pipeline(...)` — Execute stages sequentially
157
157
 
158
- **模式一:顺序模式** — 传入 stage 数组,每个 stage 收到上一个 stage 的结果:
158
+ 传入 stage 数组,每个 stage 收到上一个 stage 的结果:
159
159
 
160
160
  ```javascript
161
161
  const final = await pipeline([
@@ -164,60 +164,6 @@ const final = await pipeline([
164
164
  ]);
165
165
  ```
166
166
 
167
- **模式二:笛卡尔积模式** — 传入 items 数组 + 多个 stage,对每个 item 依次跑完所有 stage(批处理杀手锏):
168
-
169
- ```javascript
170
- // 对每个 file 依次跑 review → fix
171
- await pipeline(
172
- files, // items
173
- (file) => agent({ prompt: `Review ${file}`, description: 'review', schema: {...} }),
174
- (review, file) => agent({ prompt: `Fix ${file}: ${JSON.stringify(review)}`, description: 'fix' }),
175
- );
176
- // stage 函数签名:(prevResult, currentItem) => result;第一个 stage 只收 currentItem
177
- ```
178
-
179
- ### `workflow(name, args?)` — Call another workflow (nested orchestration)
180
-
181
- 调用已定义的子 workflow(by name),实现 workflow 嵌套编排(顺序 chain / 并行 parallel / scatter-gather / map-reduce)。被调用的 workflow 必须已通过 `workflow-script save` 或放在 `.pi/workflows/` / `~/.pi/agent/workflows/` 可被发现。
182
-
183
- **签名**:`workflow(name: string, args?: object) => Promise<AgentResult>`
184
-
185
- **参数**:
186
- - `name` — 目标 workflow 的名称(`meta.name`,即文件名 stem)
187
- - `args` — 传给子 workflow 的参数对象,子 workflow 内通过 `$ARGS` 读取
188
-
189
- **返回值**:`AgentResult`,与 `agent()` 返回结构同构:
190
- - `content: string` — 子 workflow 的 return 值(字符串化)
191
- - `parsedOutput?: unknown` — 子 workflow return 的对象(当 return 是对象时)
192
- - `usage?: {...}` — token 消耗
193
- - `error?: string` — 失败原因(成功时无此字段)
194
-
195
- **嵌套配额**:`workflow()` 调用走同一 ConcurrencyPool,按 depth 分层分配配额(`max(1, 6 - depth)`,保底 1 槽防饿死)。`parallel()` 内的 `workflow()` 调用共享父 workflow 的配额池,超出自动排队(不报错)。嵌套深度受 `MAX_FORK_DEPTH` 护栏保护(见 ADR-030 决策 3)。
196
-
197
- **返回值**:`workflow()` 返回 `AgentResult` 对象(与 `agent()` 一致):
198
- - 成功:`{ content: string, parsedOutput?: object }`——content 是子 workflow execute() 返回值的 JSON 字符串;parsedOutput 是返回值为对象时的原样回传
199
- - 失败:`{ content: "", error: string }`——子 workflow 未找到/lint 失败/执行异常/被 abort
200
-
201
- **循环检测**:`workflow()` 自动追踪调用链(A→B→C),如果目标 name 已在当前调用链中(如 A→B→A),立即返回 error result(`Circular workflow call detected: A → B → A`),不执行子 workflow。
202
-
203
- **预算继承**:子 workflow 的 token 预算继承父 workflow 的剩余预算。子 workflow 消耗的 tokens/cost 执行后累加回父 workflow 的预算池。父 workflow abort 时子 workflow 级联 abort。
204
-
205
- **chain 基础示例**(顺序:每步输出作下步输入):
206
- ```javascript
207
- const a = await workflow("extract", { source: inputPath });
208
- const b = await workflow("transform", { raw: a.content });
209
- const c = await workflow("load", { normalized: b.content });
210
- ```
211
-
212
- **parallel 基础示例**(并行:多个独立子 workflow 同时跑):
213
- ```javascript
214
- const results = await parallel(
215
- tasks.map((t) => workflow(t, { target }))
216
- );
217
- ```
218
-
219
- > 内置通用编排 workflow(chain / parallel / scatter-gather / map-reduce / review-fix-loop,可直接 `workflow run`,用 `agent()` 自包含实现)见 `extensions/universal/subagent-workflow/workflows/`。本段教 `workflow()` 嵌套 API,workflows 目录是开箱即用的通用编排工具(用 `agent()` 而非 `workflow()` 嵌套)。
220
-
221
167
  ### Other globals
222
168
 
223
169
  - `$ARGS` — Object with workflow arguments (from `--args key=val`)
@@ -158,7 +158,7 @@ describe("绝对计数语义(每帧携带当下值,非增量)", () => {
158
158
  });
159
159
  });
160
160
 
161
- describe("失败折叠 + 延迟重试直至成功一次(D5 缺席语义②)", () => {
161
+ describe("失败折叠 + 延迟重试(有界:累计 MAX_REPORT_ATTEMPTS 次放弃,D5 缺席语义②)", () => {
162
162
  it("select resolve undefined(超时/旧版 runtime)→ 折叠重试;重试帧携带完整快照;ack 后停", async () => {
163
163
  const channel = makeSelectChannel();
164
164
  const reporter = createInFlightReporter({ retryDelayMs: RETRY_MS, selectTimeoutMs: SELECT_TIMEOUT_MS });
@@ -211,6 +211,31 @@ describe("失败折叠 + 延迟重试直至成功一次(D5 缺席语义②)"
211
211
  channel.settleAll(INFLIGHT_REPORT_ACK);
212
212
  await advance(0);
213
213
  });
214
+
215
+ it("累计失败达默认上限(3 次)即放弃:不再重试,后续迁移也不再发帧", async () => {
216
+ const channel = makeSelectChannel();
217
+ // 未注入 maxAttempts:锁默认上限 MAX_REPORT_ATTEMPTS = 3(增强面降级语义)。
218
+ const reporter = createInFlightReporter({ retryDelayMs: RETRY_MS, selectTimeoutMs: SELECT_TIMEOUT_MS });
219
+ reporter.attachSession(makeCtx(channel));
220
+ await advance(0);
221
+ expect(channel.calls).toHaveLength(1);
222
+
223
+ // 三帧全部失败落定:首帧 + 2 次退避重试;第 3 次失败触顶
224
+ channel.settle(0, undefined);
225
+ await advance(RETRY_MS);
226
+ expect(channel.calls).toHaveLength(2);
227
+ channel.settle(1, undefined);
228
+ await advance(RETRY_MS);
229
+ expect(channel.calls).toHaveLength(3);
230
+ channel.settle(2, undefined);
231
+
232
+ // 达到上限:放弃为 session 内终态——时间推进与后续迁移都不再发帧
233
+ await advance(RETRY_MS * 10);
234
+ expect(channel.calls).toHaveLength(3);
235
+ reporter.onInFlightChanged();
236
+ await advance(RETRY_MS * 10);
237
+ expect(channel.calls).toHaveLength(3);
238
+ });
214
239
  });
215
240
 
216
241
  describe("不阻塞生命周期主链(D5 接线约束①)", () => {