@zhushanwen/pi-subagent-workflow 8.5.0 → 8.6.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 (92) hide show
  1. package/package.json +7 -6
  2. package/src/execution/__tests__/bg-notify-render.test.ts +73 -0
  3. package/src/execution/__tests__/chat-engine-routing.test.ts +6 -2
  4. package/src/execution/__tests__/delivery-methods.test.ts +38 -1
  5. package/src/execution/__tests__/execute-options-mapper.test.ts +11 -0
  6. package/src/execution/__tests__/execution-record.test.ts +110 -0
  7. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
  8. package/src/execution/__tests__/format-schema-instruction.test.ts +63 -32
  9. package/src/execution/__tests__/helpers/spawn-mock.ts +4 -0
  10. package/src/execution/__tests__/index-session-start.test.ts +86 -7
  11. package/src/execution/__tests__/lifecycle-manager.test.ts +46 -0
  12. package/src/execution/__tests__/list-fields.test.ts +45 -14
  13. package/src/execution/__tests__/model-resolver.test.ts +57 -5
  14. package/src/execution/__tests__/notifier-flush.test.ts +64 -26
  15. package/src/execution/__tests__/notify-ledger.test.ts +826 -0
  16. package/src/execution/__tests__/output-collector.test.ts +299 -2
  17. package/src/execution/__tests__/rpc-mode.test.ts +1 -1
  18. package/src/execution/__tests__/run-spawn-edges.test.ts +44 -1
  19. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
  20. package/src/execution/__tests__/session-runner-schema-env.test.ts +39 -0
  21. package/src/execution/__tests__/spawn-args.test.ts +37 -26
  22. package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
  23. package/src/execution/__tests__/subprocess-agent-runner.test.ts +94 -1
  24. package/src/execution/__tests__/timeout-integration.test.ts +220 -2
  25. package/src/execution/__tests__/tool-action.test.ts +92 -1
  26. package/src/execution/agent-registry.ts +6 -0
  27. package/src/execution/argv-mirror.ts +5 -1
  28. package/src/execution/concurrency-pool.ts +1 -1
  29. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +13 -0
  30. package/src/execution/engine/engines/zcode/zcode-engine.ts +11 -1
  31. package/src/execution/engine/types.ts +6 -1
  32. package/src/execution/execute-options-mapper.ts +8 -7
  33. package/src/execution/execution-record.ts +60 -1
  34. package/src/execution/lifecycle-manager.ts +23 -1
  35. package/src/execution/model-config-service.ts +16 -1
  36. package/src/execution/model-resolver.ts +31 -59
  37. package/src/execution/notifier.ts +105 -35
  38. package/src/execution/notify-ledger.ts +580 -0
  39. package/src/execution/output-collector.ts +143 -3
  40. package/src/execution/session-runner.ts +304 -71
  41. package/src/execution/subagent-service.ts +24 -2
  42. package/src/execution/subprocess-agent-runner.ts +14 -0
  43. package/src/execution/types.ts +68 -5
  44. package/src/execution/ui-request-queue.ts +14 -4
  45. package/src/index.ts +54 -1
  46. package/src/interface/__tests__/subagent-tool-path-guard.test.ts +157 -0
  47. package/src/interface/__tests__/subagent-tool-prompt.test.ts +12 -0
  48. package/src/interface/bg-notify-render.ts +33 -12
  49. package/src/interface/helpers.ts +2 -2
  50. package/src/interface/subagent-actions.ts +26 -9
  51. package/src/interface/subagent-tool-schema.ts +156 -0
  52. package/src/interface/subagent-tool.ts +56 -125
  53. package/src/interface/subagents.ts +2 -2
  54. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +16 -3
  55. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +0 -6
  56. package/src/orchestration/__tests__/agent-call-stream.test.ts +0 -5
  57. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +89 -4
  58. package/src/orchestration/__tests__/execute-agent-call.test.ts +137 -0
  59. package/src/orchestration/__tests__/jsonl-run-store-corrupt-entry.test.ts +150 -0
  60. package/src/orchestration/__tests__/jsonl-run-store-retention.test.ts +202 -0
  61. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +326 -3
  62. package/src/orchestration/__tests__/lifecycle.test.ts +41 -7
  63. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
  64. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +57 -3
  65. package/src/orchestration/__tests__/skill-discovery.test.ts +44 -0
  66. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
  67. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +43 -0
  68. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +21 -2
  69. package/src/orchestration/agent-opts-resolver.ts +104 -23
  70. package/src/orchestration/error-recovery.ts +189 -33
  71. package/src/orchestration/execute-agent-call.ts +39 -0
  72. package/src/orchestration/jsonl-run-store.ts +121 -7
  73. package/src/orchestration/launcher.ts +60 -15
  74. package/src/orchestration/lifecycle.ts +10 -7
  75. package/src/orchestration/models/__tests__/budget.test.ts +1 -61
  76. package/src/orchestration/models/budget.ts +5 -35
  77. package/src/orchestration/models/run-runtime.ts +24 -9
  78. package/src/orchestration/models/types.ts +9 -0
  79. package/src/orchestration/script-lint.ts +1 -1
  80. package/src/orchestration/skill-discovery.ts +31 -8
  81. package/src/orchestration/worker-script-builder.ts +16 -3
  82. package/src/shared/__tests__/model-ref.test.ts +306 -0
  83. package/src/shared/__tests__/schema-jsonify.test.ts +1 -1
  84. package/src/shared/__tests__/timer-delay.test.ts +61 -0
  85. package/src/shared/model-ref.ts +286 -0
  86. package/src/shared/schema-env.ts +44 -0
  87. package/src/shared/schema-jsonify.ts +6 -4
  88. package/src/shared/timer-delay.ts +54 -0
  89. package/workflows/review-fix-loop-utils.cjs +9 -7
  90. package/workflows/review-fix-loop.js +20 -12
  91. package/src/orchestration/__tests__/concurrency-gate.test.ts +0 -125
  92. package/src/orchestration/concurrency-gate.ts +0 -69
@@ -2,16 +2,30 @@
2
2
  //
3
3
  // Background 完成回注主对话。sync 不用(调用方还在 await,结果直接返回)。
4
4
  //
5
- // 职责(迁移后,U3):
5
+ // 职责(U2 后):
6
6
  // - buildLlmContent:格式化通知文案(本文件唯一逻辑职责)
7
- // - createNotifier:薄工厂,装配 @xyz-agent/session-delivery 内核
8
- // - 合并窗口 / 去重 / 退避 / flush 全部委托内核
7
+ // - createNotifier:薄工厂——ledger 装配时(bindNotifyLedgerHost 已注入)走
8
+ // 四步生命周期(写账 courier 边沿投递 回执销账 notifyId 幂等重放,
9
+ // notify-ledger.ts);未装配时退回 @xyz-agent/session-delivery 内核路径
10
+ // (合并窗口 / 去重 / 退避 / flush 委托内核,旧装配 / 无 ledger 测试兼容)
9
11
  //
10
- // 投递通道:内核 pi.sendMessage({ deliverAs:"steer", triggerTurn:true }) 注入——
11
- // 当前 turn 结束后唤醒父 agent 处理结果(steer 不打断 streaming、不锁滚动)
12
+ // 投递通道(D5 单通道化):ledger 路径经 courier settled 边沿直达
13
+ // pi.sendMessage({triggerTurn:true});内核路径的 port.send 同样只传
14
+ // {triggerTurn:true}——steer / followUp / nextTurn 通道已全部删除(nextTurn 的
15
+ // 唯一 drain 点在 session.prompt() 内,主 agent 长 streaming 场景下无限期滞留,
16
+ // 设计 D5 实测证伪);busy 场景由 ledger(settled 边沿 + isIdle 二次复查)或内核
17
+ // settled 订阅驱动在空闲边沿投递。
12
18
 
19
+ import { getLogger } from "@zhushanwen/pi-extension-logger";
13
20
  import { createDelivery, type DeliveryHandle, type DeliveryPort } from "@xyz-agent/session-delivery";
14
21
 
22
+ import { deriveOutcome } from "./execution-record.ts";
23
+ import { getBoundNotifyLedger, NOTIFY_CUSTOM_TYPE } from "./notify-ledger.ts";
24
+ import type { ClosedReason, ExecutionOutcome } from "./types.ts";
25
+
26
+ /** U4:delivery warn 出口注入用——与 index.ts 共享同一具名 logger 单例。 */
27
+ const notifyLogger = getLogger("subagents");
28
+
15
29
  /**
16
30
  * 一条待发送的完成通知记录。
17
31
  * SP-1: done/failed/crashed 合并为 closed + closedReason L2 子枚举。
@@ -24,8 +38,15 @@ export interface BgNotifyRecord {
24
38
  * toNotifyRecord 守卫放行后经此联合穷尽。
25
39
  */
26
40
  status: "running" | "closed";
27
- /** L2 关闭原因子枚举(仅 status="closed" 时有意义)。供通知文案按需展示。 */
28
- closedReason?: import("./types.ts").ClosedReason;
41
+ /** L2 关闭原因子枚举(仅 status="closed" 时有意义)。内部诊断 + outcome 兑底派生输入。 */
42
+ closedReason?: ClosedReason;
43
+ /**
44
+ * 终态三态对外语义(U3 C-outcome)。notify() 投影边界物化:closed 入参缺省时按
45
+ * deriveOutcome(closedReason, error) 兑底填充(所有可达流程下与 completeRecord
46
+ * 冻结的 record.outcome 等价——toNotifyRecord 构造点在 completeRecord 之后;该
47
+ * 构造点属 U3 领地外,不透传本字段)。buildLlmContent 与 bg-notify-render 只读本字段。
48
+ */
49
+ outcome?: ExecutionOutcome;
29
50
  agent: string;
30
51
  /** 执行所用 model(RecordSnapshot.model),用于完成通知显示。 */
31
52
  model?: string;
@@ -47,6 +68,10 @@ export interface BgNotifyRecord {
47
68
  * "Full transcript: <path>" 指针行,父 LLM 可按需读全文;one-shot 不透传,
48
69
  * 通知输出逐字节不变。缺失时 buildLlmContent 省略整行。 */
49
70
  sessionFile?: string;
71
+ /** [U2] 通知身份键(投影边界物化 = dedupe key:`id` / `id:round`)。账本条目 /
72
+ * 回执匹配 / 幂等去重共用——details 携带(不进文案,G4 字节锁定不受影响),
73
+ * 重复注入条目凭此可识别为同一条(G2 at-least-once 幂等键)。 */
74
+ notifyId?: string;
50
75
  /** [C-2] close 终态通知的轮次统计(文案 "completed after N rounds." 用)。
51
76
  * 仅 chatMode close 语义(notifyClosed)构造时携带——此时 dedup 身份 round 已被
52
77
  * 置 undefined(与轮次通知的 id:round key 区分,终态不被吞),轮数改由本字段进
@@ -57,10 +82,10 @@ export interface BgNotifyRecord {
57
82
  /** notifier 依赖的宿主最小接口(解耦,便于测试)。
58
83
  * 迁移后:仅用于构造 DeliveryPort 的底层依赖。 */
59
84
  export interface NotifierHost {
60
- /** 注入消息到主对话。 */
85
+ /** 注入消息到主对话(U2 单通道:options 只接受 triggerTurn——多通道投递已删)。 */
61
86
  sendMessage(
62
87
  message: { customType: string; content: string; display: boolean; details?: unknown },
63
- options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
88
+ options?: { triggerTurn?: boolean },
64
89
  ): void;
65
90
  /** 是否还有 running 的 background 任务(用于滑动窗口立即 flush 判断)。 */
66
91
  hasRunningBackground(): boolean;
@@ -77,9 +102,6 @@ export interface NotifierHost {
77
102
  onAgentSettled?(handler: () => void): void;
78
103
  }
79
104
 
80
- /** 发送给主对话的 customType(bg-notify-render 消费)。 */
81
- const NOTIFY_CUSTOM_TYPE = "subagent-bg-notify";
82
-
83
105
  /**
84
106
  * 将 BgNotifyRecord 格式化为 LLM 可读的 notification content。
85
107
  *
@@ -98,20 +120,19 @@ function buildLlmContent(record: BgNotifyRecord): string {
98
120
  : "";
99
121
  switch (record.status) {
100
122
  case "closed": {
101
- // v4 B-1: closed 统一终态(含 cancelled)。按 closedReason 派生通知文案。
102
- const reason = record.closedReason ?? "gc";
103
- if (reason === "cancelled") {
123
+ // U3 C-outcome:终态文案只读 outcome(notify() 投影边界已物化;?? 兑底为防御
124
+ // 完整性——单一权威函数,非同构重写)。判定先于 patchFile——失败轮也会写
125
+ // patchFile(doFinalizeRecord Step 0 对 worktreeHandle 无条件 collectPatch),
126
+ // failed 分支不展示 patch 提示,否则 worktree 失败并存时 LLM 被告知 completed
127
+ // (历史 bug 存档见 deriveOutcome 注释)。
128
+ const outcome = record.outcome ?? deriveOutcome(record.closedReason, record.error);
129
+ if (outcome === "cancelled") {
104
130
  return `Subagent "${agent}" (${id}) cancelled.`;
105
131
  }
106
- // 失败场景(gc + error):展示错误。判定必须先于 patchFile——失败轮也会写
107
- // patchFile(doFinalizeRecord Step 0 对 worktreeHandle 无条件 collectPatch),若
108
- // patch 分支在前,gc 失败 + worktree 并存时 LLM 被告知 completed。顺序与三处
109
- // 同构契约的另外两处一致(shared/subagent.ts deriveClosedDisplay、
110
- // bg-notify-render.ts renderRecordLines:cancelled → gc+error → patch/result)。
111
- if (record.error && reason === "gc") {
132
+ if (outcome === "failed") {
112
133
  return `Subagent "${agent}" (${id}) failed: ${record.error}`;
113
134
  }
114
- // 成功完成(user-close)或通用结束(gc/parent-shutdown 等):展示结果。
135
+ // 成功完成或通用结束:展示结果。
115
136
  // [C-2] chatMode close 终态通知附轮次统计(设计 D2 路径①"completed after N rounds")。
116
137
  // totalRounds 仅 close 语义携带(notifyClosed);one-shot 完成通知不设置(round 无轮次
117
138
  // 语义),文案保持 "completed. Result:" 逐字节(G4 硬约束,one-shot 字节锁测试锚定)。
@@ -150,10 +171,13 @@ export interface BgNotifier {
150
171
  }
151
172
 
152
173
  /**
153
- * 创建 Background 完成通知器(薄工厂,装配 @xyz-agent/session-delivery 内核)。
174
+ * 创建 Background 完成通知器(薄工厂)。
154
175
  *
155
- * 内核接管:gate(isIdle 退避)/ 合批窗口(滑动 60s)/ dedup(按 id:round)/ flush / shutdown flush。
156
- * 本函数职责:将 BgNotifyRecord DeliveryMessage 映射 + buildLlmContent 预格式化。
176
+ * ledger 装配时(bindNotifyLedgerHost):四步生命周期接线(写账 courier 边沿
177
+ * 投递回执销账 notifyId 幂等,见 notify-ledger.ts)。未装配时:装配
178
+ * @xyz-agent/session-delivery 内核——gate(isIdle 退避)/ 合批窗口(滑动 60s)/
179
+ * dedup(按 id:round)/ flush / shutdown flush 均委托内核。
180
+ * 本函数职责:BgNotifyRecord → 预格式化 content + notifyId 物化。
157
181
  *
158
182
  * @param host 宿主能力注入(pi.sendMessage + hasRunningBackground + isIdle)
159
183
  * @returns BgNotifier 接口(与旧类同形)
@@ -187,7 +211,12 @@ export function createNotifier(host: NotifierHost): BgNotifier {
187
211
  disposed = true;
188
212
  };
189
213
  },
190
- send: (msg, intent) => {
214
+ send: (msg, _intent) => {
215
+ // D5 单通道:投递意图唯一化——steer/followUp/nextTurn 多通道全部删除,
216
+ // 唯一发送形态 = sendCustomMessage({triggerTurn:true})。busy 场景由 ledger
217
+ //(settled 边沿 + isIdle 二次复查)或内核 settled 订阅在空闲边沿驱动,
218
+ // 不在此层分流。返回 void = 受理成功(同步无异常;SendReceipt 扩展位留待
219
+ // 升级方接入)。
191
220
  host.sendMessage(
192
221
  {
193
222
  customType: NOTIFY_CUSTOM_TYPE,
@@ -195,9 +224,7 @@ export function createNotifier(host: NotifierHost): BgNotifier {
195
224
  display: true,
196
225
  details: msg.payload.kind === "custom" ? msg.payload.details : undefined,
197
226
  },
198
- intent === "interrupt-at-turn-boundary"
199
- ? { triggerTurn: true, deliverAs: "steer" }
200
- : { triggerTurn: true, deliverAs: "followUp" },
227
+ { triggerTurn: true },
201
228
  );
202
229
  },
203
230
  };
@@ -216,6 +243,10 @@ export function createNotifier(host: NotifierHost): BgNotifier {
216
243
  // 同 key 可再入)。当前 key 空间(id / id:round,id 每 spawn 唯一)无实际差异;
217
244
  // 后续复用方勿按「60s 内不重复」假设接入(同 key 通知会被永久吞)。
218
245
  dedupe: { maxKeys: 1000 },
246
+ // U4 warn 出口参数化:内核投递失败警告接 extensionLogger(appendEntry 落
247
+ // session JSONL + XYZ_AGENT_DEBUG 落 <dataDir>/logs/),不再走 console.warn
248
+ // (stderr tee 不到日志盘——排查无痕,设计 §5 U4)。
249
+ warn: (msg, err) => notifyLogger.warn(msg, err),
219
250
  });
220
251
  let handle: DeliveryHandle = createHandle();
221
252
 
@@ -223,29 +254,68 @@ export function createNotifier(host: NotifierHost): BgNotifier {
223
254
  notify(record: BgNotifyRecord): void {
224
255
  if (disposed) return;
225
256
 
226
- // dedup key:idle(对话模式每轮完成)按 id:round 去重——不同轮次不互相掩蔽;
227
- // idleclosed/cancelled)round 恒定 undefined,key 同旧行为不变。
228
- const dedupeKey = record.round != null ? `${record.id}:${record.round}` : record.id;
257
+ // U3 C-outcome:投影边界物化 outcome——closed payload 缺省时按单一权威
258
+ // deriveOutcome 兑底填充,content 与 detailsGUI pane 消费)均携带一等 outcome;
259
+ // 所有可达流程下与 record.outcome 等价(toNotifyRecord completeRecord 之后
260
+ // 构造)。running(轮次通知)语义上无 outcome,不物化。消源自 record 的浅拷贝
261
+ // ——不改写入方对象(BgNotifyRecord 由调用方持有)。notifyId 同批物化(U2:
262
+ // dedupe key 与账本身份键同源,details 携带供回执匹配)。
263
+ const notifyId = record.round != null ? `${record.id}:${record.round}` : record.id;
264
+ const payload: BgNotifyRecord =
265
+ record.status === "closed"
266
+ ? { ...record, outcome: record.outcome ?? deriveOutcome(record.closedReason, record.error), notifyId }
267
+ : { ...record, notifyId };
229
268
 
269
+ const content = buildLlmContent(payload);
270
+
271
+ // U2 B-ledger 四步接线(设计 D4/D5):账本装配时①写账(appendEntry 先于一切
272
+ // 投递尝试)→ ②courier 边沿投递(settled 边沿 + 120s 看门狗 + isIdle 二次
273
+ // 复查)→ ③回执销账 / ④重放幂等均在 ledger 内。record 返回 false = 同
274
+ // notifyId 已在账或已销账(幂等去重,含重启恢复后的已送达账号零重发)。
275
+ const ledger = getBoundNotifyLedger();
276
+ if (ledger) {
277
+ if (!ledger.record(notifyId, content, payload)) return;
278
+ ledger.attemptDeliver();
279
+ return;
280
+ }
281
+
282
+ // 无 ledger 装配(旧装配 / 部分测试):内核路径——合批窗口 / settled 边沿 /
283
+ // dedupe(按 notifyId,key 规则与旧 dedupeKey 一致:id 或 id:round)不变。
284
+ // [C-ext-06 配套] 降级留痕:bind 缺失(含 jiti 单例分裂致跨模块读不到绑定的
285
+ // 失效形态)本是无声分岔,U2 at-least-once 在此退化为内核路径——warn 一条
286
+ // 供诊断检索,不改变向后兼容行为。
287
+ notifyLogger.warn("notify ledger not bound, falling back to delivery kernel path (at-most-once)", {
288
+ notifyId,
289
+ });
230
290
  handle.send({
231
291
  payload: {
232
292
  kind: "custom",
233
293
  customType: NOTIFY_CUSTOM_TYPE,
234
- content: buildLlmContent(record),
294
+ content,
235
295
  display: true,
236
- details: record,
296
+ details: payload,
237
297
  },
238
- dedupeKey,
298
+ dedupeKey: notifyId,
239
299
  });
240
300
  },
241
301
 
242
302
  flushPendingNotifications(): void {
303
+ // ledger 路径:立即投递尝试(isIdle 复查,busy 则挂 pending 等边沿——账已落盘,
304
+ // 重启恢复兑底);内核路径:flush。
305
+ const ledger = getBoundNotifyLedger();
306
+ if (ledger) {
307
+ ledger.attemptDeliver();
308
+ return;
309
+ }
243
310
  handle.flush();
244
311
  },
245
312
 
246
313
  dispose(): void {
247
314
  disposed = true;
248
315
  handle.dispose();
316
+ // ledger 销毁(清看门狗 + 摘模块级绑定;settled 回调由 disposed 标志静默)。
317
+ // 未 bind 时 no-op。bind 归 index.ts session_start 装配,对称免受。
318
+ getBoundNotifyLedger()?.dispose();
249
319
  },
250
320
 
251
321
  revive(): void {