@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
package/src/index.ts ADDED
@@ -0,0 +1,454 @@
1
+ /**
2
+ * subagent-workflow Extension — Factory(extension 装配点)
3
+ *
4
+ * 合并 @zhushanwen/pi-subagents + @zhushanwen/pi-workflow 为统一包。
5
+ * 注册项:3 tool(subagent + workflow + workflow-script)+ 2 command(subagents + workflows)
6
+ * + messageRenderer(subagent-bg-notify)+ pi.__workflowRun + session 事件。
7
+ *
8
+ * 三层架构:
9
+ * interface/ → 注册胶水(tools/commands/tui)
10
+ * orchestration/ → workflow engine(launcher/lifecycle/error-recovery)
11
+ * execution/ → subagents 执行运行时(SubagentService/session-runner/concurrency-pool)
12
+ *
13
+ * 设计基线:D-004(旧包不动)/ ADR-025(进程内执行)/ D-8(pi.__workflowRun 签名)。
14
+ */
15
+
16
+ import * as fs from "node:fs";
17
+ import * as os from "node:os";
18
+ import * as path from "node:path";
19
+
20
+ import type { ExtensionAPI, ExtensionContext, ResourcesDiscoverEvent, ResourcesDiscoverResult, SessionShutdownEvent, SessionStartEvent } from "@mariozechner/pi-coding-agent";
21
+ import { getAgentDir } from "@mariozechner/pi-coding-agent";
22
+
23
+ import type { AgentRegistry } from "./execution/agent-registry.ts";
24
+ import { bestEffort } from "./execution/best-effort.ts";
25
+ // ═══ execution/ 层(subagents 核心 + 运行时) ═══
26
+ import {
27
+ getModelConfigService,
28
+ ModelConfigService,
29
+ setModelConfigService,
30
+ } from "./execution/model-config-service.ts";
31
+ import { maybeCleanupExpiredSessionFiles } from "./execution/session-file-gc.ts";
32
+ import {
33
+ getSubagentService,
34
+ setSubagentService,
35
+ SubagentService,
36
+ } from "./execution/subagent-service.ts";
37
+ import { SubprocessAgentRunner } from "./execution/subprocess-agent-runner.ts";
38
+ import { WorktreeManager } from "./execution/worktree-manager.ts";
39
+ import { renderBgNotifyMessage } from "./interface/bg-notify-render.ts";
40
+ import { registerWorkflowsCommand } from "./interface/commands.ts";
41
+ import { notifyDone } from "./interface/helpers.ts";
42
+ import { registerSubagentTool } from "./interface/subagent-tool.ts";
43
+ // ═══ interface/ 层(tools/commands/tui 合并) ═══
44
+ import { registerSubagentsCommand } from "./interface/subagents.ts";
45
+ import { registerWorkflowTool } from "./interface/tool-workflow.ts";
46
+ import { registerWorkflowScriptTool } from "./interface/tool-workflow-script.ts";
47
+ import { cleanupAllTempFiles as cleanupAllFiles } from "./orchestration/agent-opts-resolver.ts";
48
+ import { JsonlRunStore } from "./orchestration/jsonl-run-store.ts";
49
+ // ═══ orchestration/ 层(workflow engine + infra) ═══
50
+ import type { LauncherDeps } from "./orchestration/launcher.ts";
51
+ import { executeNestedWorkflow, runAndWait, type WorkflowRunResult } from "./orchestration/launcher.ts";
52
+ import { pauseRun, scheduleTimeBudget } from "./orchestration/lifecycle.ts";
53
+ import type { WorkflowRun } from "./orchestration/models/workflow-run.ts";
54
+ import { WorkerHostImpl } from "./orchestration/worker-host.ts";
55
+ import { WorkflowScriptRegistryImpl } from "./orchestration/workflow-script-registry-impl.ts";
56
+
57
+ // ── pi.__workflowRun 类型扩展(D-8 签名) ─────────────────
58
+
59
+ declare module "@mariozechner/pi-coding-agent" {
60
+ interface ExtensionAPI {
61
+ __workflowRun?: (
62
+ workflowName: string,
63
+ workflowArgs: Record<string, unknown>,
64
+ workflowSignal?: AbortSignal,
65
+ workflowTimeoutMs?: number,
66
+ ) => Promise<WorkflowRunResult>;
67
+ }
68
+ }
69
+
70
+ // ── Factory ──────────────────────────────────────────────────
71
+
72
+ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
73
+ // ════════════════════════════════════════════════════════════
74
+ // subagents 域:tool + command + messageRenderer
75
+ // ════════════════════════════════════════════════════════════
76
+ registerSubagentTool(pi);
77
+ registerSubagentsCommand(pi);
78
+ pi.registerMessageRenderer("subagent-bg-notify", renderBgNotifyMessage);
79
+
80
+ // 模块级缓存:主 session 的 sessionFile(fork source 解析用)。
81
+ let cachedMainSessionFile: string | undefined;
82
+ function getCachedMainSessionFile(): string | undefined {
83
+ return cachedMainSessionFile;
84
+ }
85
+
86
+ // resources_discover:不再注入额外 skill 目录(ADR-031 废弃 discovery.json)。
87
+ // pi 核心 auto-discovery 已覆盖 .agents/skills 等标准目录,子 session 的
88
+ // --skill 由 agent({skill}) 调用方显式传入,无需 extension 额外补充。
89
+ pi.on("resources_discover", (_event: ResourcesDiscoverEvent, _ctx: ExtensionContext): ResourcesDiscoverResult => {
90
+ return {};
91
+ });
92
+
93
+ // ════════════════════════════════════════════════════════════
94
+ // workflow 域:tools + command + pi.__workflowRun + state
95
+ // ════════════════════════════════════════════════════════════
96
+ const lsRef = { lastSessionId: "" };
97
+ const notifiedRunIds = new Set<string>();
98
+ const guard = { isProcessing: false };
99
+
100
+ // Infra 实例(per-factory 单例,跨 session 复用)
101
+ const workerHost = new WorkerHostImpl();
102
+ const registry = new WorkflowScriptRegistryImpl();
103
+
104
+ // SAR 改为 per-session 构造(需要 ctxModel 填底 D-008 + subagentService 委托目标)
105
+ // old: const runner = new SubprocessAgentRunner();
106
+ // new: per-session session_start 时创建,见下方 makeDeps 前的 runner 创建
107
+
108
+ // per-session 状态(session_start 时重建)
109
+ const sessionState = new Map<
110
+ string,
111
+ {
112
+ store: JsonlRunStore;
113
+ runs: Map<string, WorkflowRun>;
114
+ activeTempFiles: Set<string>;
115
+ agentRegistry: AgentRegistry;
116
+ sessionDir: string;
117
+ /** D-008 per-session SAR(需要 ctxModel + subagentService) */
118
+ runner: SubprocessAgentRunner;
119
+ /** session 上下文(notifyDone 需要 GuiContext) */
120
+ ctx?: ExtensionContext;
121
+ /** MF-1: store 健康度。session_start 时 store.loadAll 失败则置 false,
122
+ * workflow 域启动时 fail-fast,避免后续 store.save 再次失败导致 run 状态不落地。
123
+ * subagent 域不依赖 store,不受此标志影响。 */
124
+ storeHealthy: boolean;
125
+ }
126
+ >();
127
+
128
+ function log(
129
+ level: "debug" | "info" | "warn" | "error",
130
+ component: string,
131
+ message: string,
132
+ data?: unknown,
133
+ ): void {
134
+ try {
135
+ pi.appendEntry("workflow:log", {
136
+ timestamp: Date.now(),
137
+ level,
138
+ component,
139
+ message,
140
+ data,
141
+ });
142
+ } catch (err) {
143
+ void err;
144
+ }
145
+ }
146
+
147
+ function resolveSessionDir(): string {
148
+ const defaultDir = path.join(os.homedir(), ".pi", "agent");
149
+ const sessionSlug = `--${process.cwd().replace(/^\//, "").replace(/\//g, "-")}--`;
150
+ const sessionScopedDir = path.join(os.homedir(), ".pi", "agent", "sessions", sessionSlug);
151
+ return fs.existsSync(sessionScopedDir) ? sessionScopedDir : defaultDir;
152
+ }
153
+
154
+ function makeDeps(
155
+ state: {
156
+ store: JsonlRunStore;
157
+ runs: Map<string, WorkflowRun>;
158
+ activeTempFiles: Set<string>;
159
+ agentRegistry: AgentRegistry;
160
+ sessionDir: string;
161
+ runner: SubprocessAgentRunner;
162
+ },
163
+ sessionCtx?: ExtensionContext,
164
+ ) {
165
+ const deps: LauncherDeps = {
166
+ store: state.store,
167
+ workerHost,
168
+ runner: state.runner,
169
+ runs: state.runs,
170
+ registry,
171
+ onRunDone: (run: WorkflowRun) => notifyDone(pi, run.runId, run, notifiedRunIds, sessionCtx),
172
+ agentRegistry: state.agentRegistry,
173
+ sessionDir: state.sessionDir,
174
+ activeTempFiles: state.activeTempFiles,
175
+ eventBus: pi.events,
176
+ scheduleTimeBudget: (runId: string, budgetTimeMs: number) =>
177
+ scheduleTimeBudget(runId, deps, budgetTimeMs),
178
+ onWorkflowCall: (name: string, args: Record<string, unknown>, parentRun: WorkflowRun) =>
179
+ executeNestedWorkflow(name, args, parentRun, deps),
180
+ log,
181
+ };
182
+ return deps;
183
+ }
184
+
185
+ function isScriptRunning(name: string): boolean {
186
+ for (const state of sessionState.values()) {
187
+ for (const run of state.runs.values()) {
188
+ if (run.spec.scriptName === name && run.state.status === "running") return true;
189
+ }
190
+ }
191
+ return false;
192
+ }
193
+
194
+ // ════════════════════════════════════════════════════════════
195
+ // session_start:初始化 subagents + workflow 两域
196
+ // ════════════════════════════════════════════════════════════
197
+ pi.on("session_start", async (_event: SessionStartEvent, ctx: ExtensionContext) => {
198
+ const cwd = ctx.cwd;
199
+ const agentDir = getAgentDir();
200
+ const sessionId = ctx.sessionManager.getSessionId();
201
+ lsRef.lastSessionId = sessionId;
202
+
203
+ // ── subagents 域:双 Service 装配 ──
204
+ const existingService = getSubagentService();
205
+ const existingModelService = getModelConfigService();
206
+ const modelService = existingModelService ?? new ModelConfigService({ agentDir, cwd });
207
+ const service = existingService ?? new SubagentService({ cwd, modelService, getMainSessionFile: getCachedMainSessionFile });
208
+
209
+ modelService.initModel({
210
+ modelRegistry: ctx.modelRegistry,
211
+ sessionId: ctx.sessionManager.getSessionId(),
212
+ ctxModel: ctx.model ?? undefined,
213
+ });
214
+ service.initSession({
215
+ pi,
216
+ sessionId: ctx.sessionManager.getSessionId(),
217
+ });
218
+
219
+ if (!existingService) {
220
+ setModelConfigService(modelService);
221
+ setSubagentService(service);
222
+ }
223
+
224
+ cachedMainSessionFile = ctx.sessionManager.getSessionFile() ?? undefined;
225
+
226
+ try {
227
+ maybeCleanupExpiredSessionFiles(agentDir, cwd);
228
+ } catch (err) {
229
+ void err;
230
+ console.warn("[subagents] expired session file cleanup failed:", err);
231
+ }
232
+
233
+ try {
234
+ const wtm = new WorktreeManager(agentDir);
235
+ wtm.scan();
236
+ } catch (err) {
237
+ void err;
238
+ console.warn("[subagents] worktree reaper scan failed:", err);
239
+ }
240
+
241
+ // ── workflow 域:per-session store + runs ──
242
+ const sessionDir = resolveSessionDir();
243
+ const store = new JsonlRunStore({
244
+ sessionDir,
245
+ pi,
246
+ ctx,
247
+ });
248
+ const runs = new Map<string, WorkflowRun>();
249
+
250
+ // F-4/D-003: 复用 modelService 的 AgentRegistry(统一资源发现 + 包内 builtin),
251
+ // 取代旧 orchestration/agent-discovery.ts 的 7 路径自爬。agent 发现走
252
+ // shared/resource-discovery(ADR-031),与 subagents 域共用同一份发现结果。
253
+ const agentRegistry = modelService.getAgentRegistry();
254
+
255
+ // MF-1: store 健康度跟踪。loadAll 失败 → storeHealthy=false,workflow 域启动时 fail-fast。
256
+ let storeHealthy = true;
257
+ try {
258
+ const loaded = await store.loadAll();
259
+ for (const run of loaded) {
260
+ if (run.state.status === "running") {
261
+ run.state.error = "Process killed (kill-9 or crash recovery)";
262
+ run.transition("done", "failed");
263
+ pi.events.emit("pending:unregister", {
264
+ id: run.runId,
265
+ reason: "failed",
266
+ });
267
+ }
268
+ runs.set(run.runId, run);
269
+ }
270
+ } catch (err) {
271
+ // QMF-4 fix: store.loadAll 失败是关键路径错误,workflow 域将未初始化
272
+ console.error("[subagent-workflow] store.loadAll failed, workflow domain uninitialized:", err);
273
+ storeHealthy = false;
274
+ }
275
+
276
+ // D-008: per-session SAR(需要 ctxModel 填底 + subagentService 委托目标)。
277
+ // old: const runner = new SubprocessAgentRunner()(module-level singleton,无 deps)
278
+ // new: per-session session_start 时创建,通过 sessionState 传给 makeDeps。
279
+ const runner = new SubprocessAgentRunner({
280
+ subagentService: service,
281
+ ctxModel: ctx.model ?? undefined,
282
+ });
283
+
284
+ sessionState.set(sessionId, {
285
+ store,
286
+ runs,
287
+ activeTempFiles: new Set(),
288
+ agentRegistry,
289
+ sessionDir,
290
+ runner,
291
+ ctx,
292
+ storeHealthy,
293
+ });
294
+ });
295
+
296
+ // ════════════════════════════════════════════════════════════
297
+ // model_select:用户切换 model 时刷新缓存
298
+ // ════════════════════════════════════════════════════════════
299
+ pi.on("model_select", (event: { model: NonNullable<ExtensionContext["model"]> }, _ctx: ExtensionContext) => {
300
+ const service = getModelConfigService();
301
+ if (service && typeof service.setCtxModel === "function") {
302
+ service.setCtxModel(event.model);
303
+ }
304
+ });
305
+
306
+ // ════════════════════════════════════════════════════════════
307
+ // session_tree:切分支前 pause 所有 running run
308
+ // ════════════════════════════════════════════════════════════
309
+ pi.on("session_tree", async (_event: Record<string, unknown>, ctx: ExtensionContext) => {
310
+ const sessionId = ctx.sessionManager.getSessionId();
311
+ lsRef.lastSessionId = sessionId;
312
+
313
+ const state = sessionState.get(sessionId);
314
+ if (state) {
315
+ for (const run of state.runs.values()) {
316
+ if (run.state.status === "running") {
317
+ try {
318
+ await pauseRun(run.runId, makeDeps(state, ctx));
319
+ } catch (err) {
320
+ bestEffort(err, "pauseRun (session_tree handler)");
321
+ }
322
+ }
323
+ }
324
+ }
325
+ });
326
+
327
+ // ════════════════════════════════════════════════════════════
328
+ // session_shutdown:dispose subagents + pause workflows + cleanup
329
+ // ════════════════════════════════════════════════════════════
330
+ pi.on("session_shutdown", async (_event: SessionShutdownEvent, _ctx: ExtensionContext) => {
331
+ // ── subagents 域:dispose SubagentService ──
332
+ getSubagentService()?.dispose();
333
+
334
+ // ── workflow 域:pause 所有 running run + 清理 temp files ──
335
+ // H-5: 遍历所有 sessionState 条目清理(而不只 lastSessionId——
336
+ // 防御 session 切换但 session_tree 未先触发导致 lastSessionId 指向已删除 session 的情况)。
337
+ for (const [sessionId, state] of sessionState) {
338
+ const running = Array.from(state.runs.values()).filter((r) => r.state.status === "running");
339
+ await Promise.allSettled(
340
+ running.map((run) => pauseRun(run.runId, makeDeps(state, _ctx))),
341
+ );
342
+ cleanupAllFiles(state.activeTempFiles);
343
+ sessionState.delete(sessionId);
344
+ }
345
+ });
346
+
347
+ // ════════════════════════════════════════════════════════════
348
+ // pi.__workflowRun(D-8 签名)
349
+ // ════════════════════════════════════════════════════════════
350
+ pi.__workflowRun = async (
351
+ workflowName: string,
352
+ workflowArgs: Record<string, unknown>,
353
+ workflowSignal?: AbortSignal,
354
+ workflowTimeoutMs?: number,
355
+ ): Promise<WorkflowRunResult> => {
356
+ // 注意:lastSessionId 是单值假设——Pi 当前保证单 session 串行(一次只一个活跃 session)。
357
+ // 若未来 Pi 支持多 session 并发,此处需改为从 ctx.sessionManager.getSessionId() 显式传入。
358
+ // M-2 已记录此假设。
359
+ const state = sessionState.get(lsRef.lastSessionId);
360
+ if (!state) {
361
+ return {
362
+ status: "done",
363
+ reason: "failed",
364
+ error: "Session not initialized",
365
+ runId: "",
366
+ };
367
+ }
368
+ // MF-1: store 不健康时 fail-fast,避免 store.save 再次失败导致 run 状态不落地。
369
+ if (!state.storeHealthy) {
370
+ return {
371
+ status: "done",
372
+ reason: "failed",
373
+ error: "Workflow store unavailable (loadAll failed in session_start)",
374
+ runId: "",
375
+ };
376
+ }
377
+ return runAndWait(
378
+ workflowName,
379
+ workflowArgs,
380
+ makeDeps(state, state.ctx),
381
+ workflowSignal,
382
+ workflowTimeoutMs,
383
+ );
384
+ };
385
+
386
+ // ════════════════════════════════════════════════════════════
387
+ // Tools(3 个)—— lazy deps 注入
388
+ // ════════════════════════════════════════════════════════════
389
+ const getDeps = () => {
390
+ // 注意:lastSessionId 是单值假设——Pi 当前保证单 session 串行(一次只一个活跃 session)。
391
+ // 若未来 Pi 支持多 session 并发,此处需改为从 ctx.sessionManager.getSessionId() 显式传入。
392
+ // M-2 已记录此假设。
393
+ const state = sessionState.get(lsRef.lastSessionId);
394
+ if (!state) throw new Error("Session not initialized");
395
+ // MF-1: store 不健康时 fail-fast,避免 store.save 再次失败导致 run 状态不落地。
396
+ if (!state.storeHealthy) {
397
+ throw new Error("Workflow store unavailable (loadAll failed in session_start)");
398
+ }
399
+ return makeDeps(state, state.ctx);
400
+ };
401
+
402
+ const lazyDeps: LauncherDeps = {
403
+ get store() {
404
+ return getDeps().store;
405
+ },
406
+ workerHost,
407
+ get runner() {
408
+ return getDeps().runner;
409
+ },
410
+ get runs() {
411
+ return getDeps().runs;
412
+ },
413
+ registry,
414
+ get onRunDone() {
415
+ return getDeps().onRunDone;
416
+ },
417
+ get agentRegistry() {
418
+ return getDeps().agentRegistry;
419
+ },
420
+ get sessionDir() {
421
+ return getDeps().sessionDir;
422
+ },
423
+ get activeTempFiles() {
424
+ return getDeps().activeTempFiles;
425
+ },
426
+ get eventBus() {
427
+ return getDeps().eventBus;
428
+ },
429
+ get scheduleTimeBudget() {
430
+ return getDeps().scheduleTimeBudget;
431
+ },
432
+ get onWorkflowCall() {
433
+ return getDeps().onWorkflowCall;
434
+ },
435
+ get log() {
436
+ return getDeps().log;
437
+ },
438
+ };
439
+
440
+ registerWorkflowTool(pi, lazyDeps, guard);
441
+ registerWorkflowScriptTool(pi, registry, isScriptRunning);
442
+
443
+ // ════════════════════════════════════════════════════════════
444
+ // Commands(2 个)
445
+ // ════════════════════════════════════════════════════════════
446
+ registerWorkflowsCommand(
447
+ pi,
448
+ () => {
449
+ const state = sessionState.get(lsRef.lastSessionId);
450
+ return state?.runs ?? new Map();
451
+ },
452
+ lazyDeps,
453
+ );
454
+ }