@zhushanwen/pi-subagent-workflow 0.1.0 → 0.3.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 (130) hide show
  1. package/README.md +56 -0
  2. package/agents/context-builder.md +1 -3
  3. package/agents/explorer.md +27 -0
  4. package/agents/oracle.md +2 -2
  5. package/agents/orchestrator.md +48 -0
  6. package/agents/planner.md +1 -3
  7. package/agents/researcher.md +0 -2
  8. package/agents/reviewer.md +2 -2
  9. package/agents/worker.md +0 -2
  10. package/package.json +5 -3
  11. package/skills/workflow-script-format/SKILL.md +6 -6
  12. package/src/execution/__tests__/agent-registry.test.ts +3 -3
  13. package/src/execution/__tests__/agent-result-mapper.test.ts +24 -2
  14. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +484 -0
  15. package/src/execution/__tests__/channel-registry-handshake.test.ts +233 -0
  16. package/src/execution/__tests__/concurrency-pool.test.ts +33 -0
  17. package/src/execution/__tests__/crash-recovery.test.ts +5 -1
  18. package/src/execution/__tests__/dialog-queue.test.ts +299 -0
  19. package/src/execution/__tests__/execute-nesting.test.ts +1 -1
  20. package/src/execution/__tests__/execute-options-mapper.test.ts +41 -9
  21. package/src/execution/__tests__/finalize-record.test.ts +173 -0
  22. package/src/execution/__tests__/gui-mode-dispatch.test.ts +59 -0
  23. package/src/execution/__tests__/helpers/spawn-mock.ts +209 -0
  24. package/src/execution/__tests__/host-mode.test.ts +87 -0
  25. package/src/execution/__tests__/index-session-start.test.ts +342 -0
  26. package/src/execution/__tests__/list-component.test.ts +1 -1
  27. package/src/execution/__tests__/notifier-flush.test.ts +78 -0
  28. package/src/execution/__tests__/path-encoding.test.ts +30 -1
  29. package/src/execution/__tests__/record-store.test.ts +86 -2
  30. package/src/execution/__tests__/records-cwd-isolation.test.ts +91 -0
  31. package/src/execution/__tests__/rpc-mode.test.ts +89 -0
  32. package/src/execution/__tests__/run-spawn-edges.test.ts +157 -153
  33. package/src/execution/__tests__/run-spawn-integration.test.ts +85 -151
  34. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +193 -0
  35. package/src/execution/__tests__/sdk-contract.test.ts +5 -2
  36. package/src/execution/__tests__/session-file-gc.test.ts +46 -0
  37. package/src/execution/__tests__/session-reconstructor.test.ts +20 -0
  38. package/src/execution/__tests__/session-start-reaper.test.ts +7 -1
  39. package/src/execution/__tests__/spawn-args.test.ts +14 -19
  40. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +189 -0
  41. package/src/execution/__tests__/stdin-writer.test.ts +353 -0
  42. package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
  43. package/src/execution/__tests__/subagent-service.test.ts +73 -3
  44. package/src/execution/__tests__/subprocess-agent-runner.test.ts +72 -3
  45. package/src/execution/__tests__/tool-action.test.ts +27 -5
  46. package/src/execution/__tests__/ui-channels.test.ts +187 -0
  47. package/src/execution/__tests__/ui-interaction-model.test.ts +67 -0
  48. package/src/execution/__tests__/ui-request-handler-factory.test.ts +166 -0
  49. package/src/execution/__tests__/ui-request-handler.test.ts +204 -0
  50. package/src/execution/__tests__/ui-request-observability.test.ts +101 -0
  51. package/src/execution/__tests__/ui-request-queue.test.ts +133 -0
  52. package/src/execution/__tests__/worktree-manager.test.ts +1 -1
  53. package/src/execution/agent-registry.ts +1 -1
  54. package/src/execution/agent-result-mapper.ts +4 -1
  55. package/src/execution/channel-registry-access.ts +138 -0
  56. package/src/execution/concurrency-pool.ts +38 -6
  57. package/src/execution/dialog-queue.ts +329 -0
  58. package/src/execution/execute-options-mapper.ts +21 -4
  59. package/src/execution/execution-record.ts +5 -0
  60. package/src/execution/finalize-record.ts +160 -0
  61. package/src/execution/get-state-handshake.ts +104 -0
  62. package/src/execution/host-mode.ts +52 -0
  63. package/src/execution/manifest-store.ts +206 -0
  64. package/src/execution/notifier.ts +5 -1
  65. package/src/execution/path-encoding.ts +18 -0
  66. package/src/execution/pi-invocation.ts +1 -1
  67. package/src/execution/record-store.ts +110 -2
  68. package/src/execution/session-file-gc.ts +25 -3
  69. package/src/execution/session-reconstructor.ts +11 -0
  70. package/src/execution/session-runner.ts +228 -32
  71. package/src/execution/spawn-event-adapter.ts +219 -6
  72. package/src/execution/stdin-writer.ts +106 -0
  73. package/src/execution/stream-sink.ts +83 -0
  74. package/src/execution/subagent-service.ts +230 -235
  75. package/src/execution/subprocess-agent-runner.ts +16 -4
  76. package/src/execution/types.ts +23 -3
  77. package/src/execution/ui-channels.ts +216 -0
  78. package/src/execution/ui-interaction-model.ts +48 -0
  79. package/src/execution/ui-request-handler-factory.ts +175 -0
  80. package/src/execution/ui-request-observability.ts +77 -0
  81. package/src/execution/ui-request-queue.ts +168 -0
  82. package/src/index.ts +101 -4
  83. package/src/interface/__tests__/subagent-tool-prompt.test.ts +84 -0
  84. package/src/interface/__tests__/workflow-state-file-exposure.test.ts +38 -0
  85. package/src/interface/__tests__/workflow-tool-prompt.test.ts +50 -0
  86. package/src/interface/command-actions.ts +77 -0
  87. package/src/interface/commands.ts +40 -4
  88. package/src/interface/format.ts +2 -0
  89. package/src/interface/gui-mappers.ts +83 -0
  90. package/src/interface/helpers.ts +52 -9
  91. package/src/interface/list-component.ts +3 -1
  92. package/src/interface/subagent-actions.ts +44 -24
  93. package/src/interface/subagent-tool.ts +56 -24
  94. package/src/interface/subagents.ts +45 -5
  95. package/src/interface/tool-render.ts +16 -5
  96. package/src/interface/tool-workflow-script.ts +113 -15
  97. package/src/interface/tool-workflow.ts +92 -34
  98. package/src/interface/views/WorkflowsView.ts +13 -4
  99. package/src/interface/views/__tests__/detail-content-session-file.test.ts +70 -0
  100. package/src/interface/views/detail-content.ts +20 -0
  101. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +208 -0
  102. package/src/orchestration/__tests__/agent-call-stream.test.ts +157 -0
  103. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +2 -0
  104. package/src/orchestration/__tests__/execute-agent-call.test.ts +171 -0
  105. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +177 -0
  106. package/src/orchestration/__tests__/worker-script-builder.test.ts +15 -0
  107. package/src/orchestration/agent-opts-resolver.ts +11 -2
  108. package/src/orchestration/error-recovery.ts +131 -23
  109. package/src/orchestration/execute-agent-call.ts +12 -3
  110. package/src/orchestration/jsonl-run-store.ts +10 -0
  111. package/src/orchestration/lifecycle.ts +1 -1
  112. package/src/orchestration/models/agent-call.ts +7 -0
  113. package/src/orchestration/models/ports.ts +15 -2
  114. package/src/orchestration/models/run-spec.ts +6 -0
  115. package/src/orchestration/models/trace.ts +1 -0
  116. package/src/orchestration/models/types.ts +19 -0
  117. package/src/orchestration/node-ops.ts +2 -0
  118. package/src/orchestration/worker-script-builder.ts +1 -0
  119. package/workflows/README.md +58 -0
  120. package/workflows/chain.js +107 -0
  121. package/workflows/map-reduce.js +142 -0
  122. package/workflows/parallel.js +131 -0
  123. package/workflows/scatter-gather.js +146 -0
  124. package/agents/scout.md +0 -17
  125. package/examples/README.md +0 -43
  126. package/examples/chain.example.js +0 -92
  127. package/examples/map-reduce.example.js +0 -99
  128. package/examples/parallel.example.js +0 -82
  129. package/examples/scatter-gather.example.js +0 -106
  130. package/src/interface/gui-adapter.ts +0 -136
@@ -12,6 +12,7 @@ import * as fs from "node:fs";
12
12
  import * as path from "node:path";
13
13
 
14
14
  import { getCurrentActivity, getDisplayItems, getEventLog, markReconstructedStatus, snapshot as toSnapshot } from "./execution-record.ts";
15
+ import type { ManifestRecord, ManifestStore } from "./manifest-store.ts";
15
16
  import { reconstructFromFile } from "./session-reconstructor.ts";
16
17
  import type {
17
18
  ExecutionRecord,
@@ -37,7 +38,25 @@ const STATUS_PRIORITY: Record<ExecutionStatus, number> = {
37
38
  };
38
39
 
39
40
  /** .alive sidecar 的 24 小时软超时(超过此时间即使 pid 存活也判 crashed)。 */
40
- const ALIVE_SOFT_TIMEOUT_MS = 86_400_000; // 24h in ms
41
+ const ALIVE_SOFT_TIMEOUT_MS = 3_600_000; // 1h in ms (reduced from 24h to minimize PID reuse window)
42
+
43
+ /**
44
+ * manifest status → ExecutionStatus 运行时守卫映射。
45
+ *
46
+ * manifest 写 running/completed/failed/cancelled 四态(ManifestRecord.status union),但磁盘
47
+ * 文件可能陈旧(含历史 "error" 值、被外部篡改、或意外出现 crashed 值)。越界值返回 null——
48
+ * manifestToSubagent 据此返回 null,collectRecords 跳过损坏 record 并 console.warn,不因单个
49
+ * 坏文件崩溃,也不把损坏 record 错误降级为 failed(failed 会触发重试/告警,是误报)。
50
+ *
51
+ * 提取为纯函数:同时解决 PR#85 反射问题(三元 + `as ExecutionStatus` cast)。
52
+ */
53
+ function mapManifestStatus(s: string): ExecutionStatus | null {
54
+ if (s === "completed") return "done";
55
+ if (s === "failed") return "failed";
56
+ if (s === "running") return "running";
57
+ if (s === "cancelled") return "cancelled";
58
+ return null; // 越界=数据损坏(含历史 "error"、意外 crashed 值),返回 null 让调用方跳过
59
+ }
41
60
 
42
61
  /** store 变更监听器(返回取消订阅函数)。 */
43
62
  export type ChangeListener = () => void;
@@ -45,6 +64,12 @@ export type ChangeListener = () => void;
45
64
  /** status 过滤模式(collectRecords 的核心能力参数)。 */
46
65
  export type StatusFilter = "running" | "all";
47
66
 
67
+ /** Pi ExtensionAPI 的最小子集(仅 collectRecords 跳过损坏 manifest 时上报用)。
68
+ * 解构为局部类型,避免与 subagent-service 的 PiLike 循环依赖。 */
69
+ export type RecordStorePi = {
70
+ appendEntry?: (customType: string, data: unknown) => void;
71
+ } | null | undefined;
72
+
48
73
  // ============================================================
49
74
  // RecordStore
50
75
  // ============================================================
@@ -62,11 +87,31 @@ export class RecordStore {
62
87
  private readonly records = new Map<string, ExecutionRecord>();
63
88
  private readonly listeners = new Set<ChangeListener>();
64
89
  private _disposed = false;
90
+ /** Pi handle(用于 appendEntry 上报损坏 manifest)。构造时可空,setPi() 后续注入。
91
+ * 显式存为字段而非构造参数 readonly:setPi 需要写权限。 */
92
+ private pi: RecordStorePi = null;
65
93
 
66
94
  /** 重建缓存:sessionFile → SubagentRecord。notifyChange 时失效。 */
67
95
  private reconCache: Map<string, SubagentRecord> | undefined;
68
96
 
69
- constructor(private readonly sessionsDir: string) {}
97
+ constructor(
98
+ private readonly sessionsDir: string,
99
+ private readonly manifestStore?: ManifestStore,
100
+ /** Pi 入口(注入 appendEntry 用于上报损坏 manifest)。
101
+ * SubagentService 构造时 this.pi 尚未注入(session_start 之前),传 undefined 兜底;
102
+ * 后续通过 setPi() 注入(见下)。允许 null = 兼容 PiLike 字段类型。 */
103
+ pi?: RecordStorePi,
104
+ ) {
105
+ this.pi = pi ?? null;
106
+ }
107
+
108
+ /** session_start 后由 SubagentService.initSession 调,注入真实 Pi handle。
109
+ * 设计为独立方法而非要求构造时必传——RecordStore 在 SubagentService 构造时即建
110
+ * (与 sessionsDir/manifestStore 一同初始化),但 this.pi 此时尚未注入。
111
+ * 后续构造期外的 appendEntry 上报才有意义。 */
112
+ setPi(pi: RecordStorePi): void {
113
+ this.pi = pi ?? null;
114
+ }
70
115
 
71
116
  /** 注册新 record。触发 onChange。 */
72
117
  register(record: ExecutionRecord): void {
@@ -154,6 +199,32 @@ export class RecordStore {
154
199
  byId.set(rec.id, rec);
155
200
  }
156
201
 
202
+ // 1.5 FR-8: manifest 源补充 orphan 记录。
203
+ // 优先级:内存 > 磁盘重建 > manifest。manifest 仅补充 session.jsonl 重建失败的记录。
204
+ if (this.manifestStore) {
205
+ for (const manifest of this.readManifestsSync()) {
206
+ if (byId.has(manifest.id)) continue; // 已被磁盘/内存源覆盖
207
+ if (rootSessionFilter !== undefined && manifest.rootSessionId !== rootSessionFilter) continue;
208
+ const rec = RecordStore.manifestToSubagent(manifest);
209
+ if (!rec) {
210
+ // manifest status 越界=数据损坏(含历史 "error"、意外 crashed 值):跳过而非降级 failed,
211
+ // 避免损坏 record 被误显示为 failed(触发错误重试/告警)。
212
+ // 双通道上报:console.warn 给开发者(终端调试);pi.appendEntry 给用户(session 内可见,
213
+ // 即使退出后也能从 session.jsonl 复盘事故原因)。SubagentService 构造时 pi 未注入
214
+ // (session_start 之前),appendEntry 走可选链安全降级。
215
+ console.warn("[subagents] skip manifest with invalid status:", manifest.id, manifest.status);
216
+ this.pi?.appendEntry?.("subagent:manifest-invalid-status", {
217
+ id: manifest.id,
218
+ status: manifest.status,
219
+ rootSessionId: manifest.rootSessionId,
220
+ agentName: manifest.agentName,
221
+ });
222
+ continue;
223
+ }
224
+ byId.set(rec.id, rec);
225
+ }
226
+ }
227
+
157
228
  // 2. 内存源覆盖(running record 优先——它是活态,比磁盘重建更新鲜)。同样按 session 过滤。
158
229
  for (const r of this.records.values()) {
159
230
  if (rootSessionFilter !== undefined && r.rootSessionId !== rootSessionFilter) continue;
@@ -250,6 +321,7 @@ export class RecordStore {
250
321
  const rec: SubagentRecord = {
251
322
  id: recon.id,
252
323
  agent: recon.agent,
324
+ slug: recon.slug,
253
325
  status: recon.status, // 临时值,各分支覆盖
254
326
  mode: recon.mode,
255
327
  startedAt: recon.startedAt,
@@ -322,6 +394,41 @@ export class RecordStore {
322
394
  return b.startedAt - a.startedAt; // 新→旧
323
395
  }
324
396
 
397
+ /** FR-8: 同步读取所有 manifest 记录(封装 ManifestStore.listAllSync,消除反射访问)。 */
398
+ private readManifestsSync(): readonly ManifestRecord[] {
399
+ return this.manifestStore?.listAllSync() ?? [];
400
+ }
401
+
402
+ /** FR-8: ManifestRecord → SubagentRecord(manifest 源投影)。
403
+ * task/slug/model 从 manifest 真实值投影(配合 writeManifest 补字段),缺失兜底空串。
404
+ * status 越界(mapManifestStatus 返回 null)时返回 null,由 collectRecords 跳过。 */
405
+ private static manifestToSubagent(m: ManifestRecord): SubagentRecord | null {
406
+ const status = mapManifestStatus(m.status);
407
+ if (status === null) return null;
408
+ return {
409
+ id: m.id,
410
+ agent: m.agentName,
411
+ task: m.task ?? "",
412
+ slug: m.slug ?? "",
413
+ status,
414
+ mode: "background" as const,
415
+ startedAt: m.createdAt,
416
+ rootSessionId: m.rootSessionId || undefined,
417
+ parentRecordId: undefined,
418
+ depth: 0,
419
+ endedAt: m.completedAt,
420
+ turns: 0,
421
+ totalTokens: 0,
422
+ model: m.model ?? "",
423
+ thinkingLevel: undefined,
424
+ eventLog: [],
425
+ displayItems: [],
426
+ result: undefined,
427
+ error: status === "failed" ? "manifest record" : undefined,
428
+ sessionFile: m.sessionFile,
429
+ };
430
+ }
431
+
325
432
  /** ExecutionRecord → SubagentRecord(内存源投影)。 */
326
433
  private static recordToSubagent(r: ExecutionRecord): SubagentRecord {
327
434
  return {
@@ -329,6 +436,7 @@ export class RecordStore {
329
436
  agent: r.agent,
330
437
  status: r.status,
331
438
  mode: r.mode,
439
+ slug: r.slug,
332
440
  startedAt: r.startedAt,
333
441
  rootSessionId: r.rootSessionId,
334
442
  parentRecordId: r.parentRecordId,
@@ -44,8 +44,11 @@ export function maybeCleanupExpiredSessionFiles(agentDir: string, cwd: string):
44
44
  }
45
45
  }
46
46
 
47
- /** 递归扫描目录,unlink 超 TTL 的 .jsonl 文件及其 .cancelled sidecar。 */
48
- function walkAndClean(dir: string, now: number): void {
47
+ /** 递归扫描目录,unlink 超 TTL 的 .jsonl 文件及其 .cancelled sidecar。
48
+ * [F2] 进入名为 records 的子目录时,额外清理超 TTL 的 manifest .json(跳过 .tmp.——
49
+ * recoverTmpFiles 同步处理)。allowManifestJson 仅由父调用按目录名开启,其他位置
50
+ * (如 subagents/worktrees.json)不匹配 .json,避免误删 worktree reaper 状态文件。 */
51
+ function walkAndClean(dir: string, now: number, allowManifestJson = false): void {
49
52
  let entries: fs.Dirent[];
50
53
  try {
51
54
  entries = fs.readdirSync(dir, { withFileTypes: true });
@@ -56,7 +59,26 @@ function walkAndClean(dir: string, now: number): void {
56
59
  for (const entry of entries) {
57
60
  const full = path.join(dir, entry.name);
58
61
  if (entry.isDirectory()) {
59
- walkAndClean(full, now);
62
+ // 只在进入名为 records 的子目录时打开 manifest .json 清理。
63
+ // records 在 <enc>/records/ 下递归自动覆盖;其他位置(如 subagents/worktrees.json)
64
+ // 不能匹配 .json——否则会误删 worktree reaper 依赖的状态文件。
65
+ walkAndClean(full, now, entry.name === "records");
66
+ } else if (
67
+ allowManifestJson &&
68
+ entry.name.endsWith(".json") &&
69
+ // 跳过 .tmp.:recoverTmpFiles(session_start)同步处理 tmp,GC 不重复。
70
+ // 不校验内容——30 天 mtime 已是强 orphan 信号,扩展名 + 文件名足够。
71
+ !entry.name.includes(".tmp.")
72
+ ) {
73
+ try {
74
+ const stat = fs.statSync(full);
75
+ if (now - stat.mtimeMs > TTL_MS) {
76
+ fs.unlinkSync(full);
77
+ }
78
+ } catch (_e) {
79
+ // 文件可能已被删除,忽略
80
+ void _e;
81
+ }
60
82
  } else if (entry.name.endsWith(".jsonl")) {
61
83
  try {
62
84
  const stat = fs.statSync(full);
@@ -84,6 +84,11 @@ export interface SubagentIdentityData {
84
84
  agent: string;
85
85
  mode: ExecutionMode;
86
86
  task: string;
87
+ /**
88
+ * 短标签(≤20 字符)。旧文件缺失→读取兜底空串(见 reconstructFromFile)。
89
+ * 写入时(session-runner)必填;读取时可选以兼容旧文件。
90
+ */
91
+ slug?: string;
87
92
  startedAt: number;
88
93
  /** 根 Pi session ID(session 隔离过滤用)。旧文件可能缺失。 */
89
94
  rootSessionId?: string;
@@ -126,6 +131,8 @@ export interface ReconstructedRecord {
126
131
  agent: string;
127
132
  mode: ExecutionMode;
128
133
  task: string;
134
+ /** 短标签(≤20 字符)。旧文件缺失→兜底空串。 */
135
+ slug: string;
129
136
  startedAt: number;
130
137
  /** 根 Pi session ID(session 隔离过滤用)。旧文件可能缺失。 */
131
138
  rootSessionId: string | undefined;
@@ -353,6 +360,7 @@ export function reconstructFromFile(sessionFile: string): ReconstructedRecord |
353
360
  const turn = emptyTurn();
354
361
  turns.push(turn);
355
362
 
363
+ if (!Array.isArray(msg.content)) continue;
356
364
  for (const block of msg.content) {
357
365
  if (block.type === "text") {
358
366
  turn.text += block.text;
@@ -428,8 +436,11 @@ export function reconstructFromFile(sessionFile: string): ReconstructedRecord |
428
436
 
429
437
  // rootSessionId 归一化:新文件读 rootSessionId,旧文件 fallback parentSessionId。
430
438
  const rootSessionId = identity.rootSessionId ?? identity.parentSessionId;
439
+ // slug 兜底:旧 identity entry 无 slug 字段 → 空串(与 SubagentRecord.slug: string 契约一致)。
440
+ const slug = identity.slug ?? "";
431
441
  return {
432
442
  ...identity,
443
+ slug,
433
444
  rootSessionId,
434
445
  parentRecordId: identity.parentRecordId,
435
446
  depth: identity.depth ?? 0,