stageflow 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 (180) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/dist/agent/activity.d.ts +76 -0
  4. package/dist/agent/activity.js +89 -0
  5. package/dist/agent/activityObserver.d.ts +17 -0
  6. package/dist/agent/activityObserver.js +66 -0
  7. package/dist/agent/cursorExtension.d.ts +12 -0
  8. package/dist/agent/cursorExtension.js +88 -0
  9. package/dist/agent/cursorProvider.d.ts +14 -0
  10. package/dist/agent/cursorProvider.js +139 -0
  11. package/dist/agent/fakeAgent.d.ts +29 -0
  12. package/dist/agent/fakeAgent.js +295 -0
  13. package/dist/agent/oauthSessionManager.d.ts +33 -0
  14. package/dist/agent/oauthSessionManager.js +214 -0
  15. package/dist/agent/piAdapter.d.ts +128 -0
  16. package/dist/agent/piAdapter.js +967 -0
  17. package/dist/agent/port.d.ts +63 -0
  18. package/dist/agent/port.js +46 -0
  19. package/dist/agent/providerAuth.d.ts +101 -0
  20. package/dist/agent/providerAuth.js +282 -0
  21. package/dist/agent/providerSupport.d.ts +28 -0
  22. package/dist/agent/providerSupport.js +19 -0
  23. package/dist/cli/providersCommand.d.ts +11 -0
  24. package/dist/cli/providersCommand.js +275 -0
  25. package/dist/cli/terminalAuthInteraction.d.ts +8 -0
  26. package/dist/cli/terminalAuthInteraction.js +65 -0
  27. package/dist/cli/terminalSecret.d.ts +2 -0
  28. package/dist/cli/terminalSecret.js +67 -0
  29. package/dist/cli/validateCommand.d.ts +11 -0
  30. package/dist/cli/validateCommand.js +81 -0
  31. package/dist/cli/validateOutput.d.ts +8 -0
  32. package/dist/cli/validateOutput.js +75 -0
  33. package/dist/cli.d.ts +2 -0
  34. package/dist/cli.js +273 -0
  35. package/dist/config/createPipeline.d.ts +31 -0
  36. package/dist/config/createPipeline.js +255 -0
  37. package/dist/config/createStage.d.ts +25 -0
  38. package/dist/config/createStage.js +181 -0
  39. package/dist/config/listConfig.d.ts +33 -0
  40. package/dist/config/listConfig.js +195 -0
  41. package/dist/config/listExtensions.d.ts +26 -0
  42. package/dist/config/listExtensions.js +50 -0
  43. package/dist/config/listSkills.d.ts +29 -0
  44. package/dist/config/listSkills.js +55 -0
  45. package/dist/config/loadOutcome.d.ts +27 -0
  46. package/dist/config/loadOutcome.js +6 -0
  47. package/dist/config/loadPipeline.d.ts +25 -0
  48. package/dist/config/loadPipeline.js +196 -0
  49. package/dist/config/loadStage.d.ts +4 -0
  50. package/dist/config/loadStage.js +127 -0
  51. package/dist/config/loadTask.d.ts +7 -0
  52. package/dist/config/loadTask.js +69 -0
  53. package/dist/config/readYamlObject.d.ts +1 -0
  54. package/dist/config/readYamlObject.js +5 -0
  55. package/dist/config/resolvePipelineDag.d.ts +12 -0
  56. package/dist/config/resolvePipelineDag.js +282 -0
  57. package/dist/config/validateCatalog.d.ts +36 -0
  58. package/dist/config/validateCatalog.js +322 -0
  59. package/dist/envelope/check.d.ts +3 -0
  60. package/dist/envelope/check.js +52 -0
  61. package/dist/envelope/payloadSchema.d.ts +10 -0
  62. package/dist/envelope/payloadSchema.js +91 -0
  63. package/dist/hitl/qaTrail.d.ts +31 -0
  64. package/dist/hitl/qaTrail.js +65 -0
  65. package/dist/index.d.ts +13 -0
  66. package/dist/index.js +7 -0
  67. package/dist/mcp/projectRun.d.ts +37 -0
  68. package/dist/mcp/projectRun.js +57 -0
  69. package/dist/mcp/readArtifact.d.ts +2 -0
  70. package/dist/mcp/readArtifact.js +46 -0
  71. package/dist/mcp/server.d.ts +7 -0
  72. package/dist/mcp/server.js +26 -0
  73. package/dist/mcp/tools.d.ts +9 -0
  74. package/dist/mcp/tools.js +96 -0
  75. package/dist/package-meta.d.ts +1 -0
  76. package/dist/package-meta.js +1 -0
  77. package/dist/prompt/priorEnvelope.d.ts +2 -0
  78. package/dist/prompt/priorEnvelope.js +5 -0
  79. package/dist/runstore/catalog.d.ts +8 -0
  80. package/dist/runstore/catalog.js +16 -0
  81. package/dist/runstore/createStore.d.ts +15 -0
  82. package/dist/runstore/createStore.js +23 -0
  83. package/dist/runstore/disk/DiskRunStore.d.ts +30 -0
  84. package/dist/runstore/disk/DiskRunStore.js +238 -0
  85. package/dist/runstore/disk/catalogHelpers.d.ts +11 -0
  86. package/dist/runstore/disk/catalogHelpers.js +102 -0
  87. package/dist/runstore/layout.d.ts +22 -0
  88. package/dist/runstore/layout.js +58 -0
  89. package/dist/runstore/paths.d.ts +7 -0
  90. package/dist/runstore/paths.js +60 -0
  91. package/dist/runstore/pipelineDagSnapshot.d.ts +5 -0
  92. package/dist/runstore/pipelineDagSnapshot.js +43 -0
  93. package/dist/runstore/port.d.ts +146 -0
  94. package/dist/runstore/port.js +28 -0
  95. package/dist/runstore/runProjection.d.ts +6 -0
  96. package/dist/runstore/runProjection.js +102 -0
  97. package/dist/runstore/sqlite/SqliteRunStore.d.ts +48 -0
  98. package/dist/runstore/sqlite/SqliteRunStore.js +460 -0
  99. package/dist/runstore/sqlite/migrateFromDisk.d.ts +5 -0
  100. package/dist/runstore/sqlite/migrateFromDisk.js +108 -0
  101. package/dist/runstore/sqlite/schema.d.ts +1 -0
  102. package/dist/runstore/sqlite/schema.js +59 -0
  103. package/dist/runstore/stageExecution.d.ts +13 -0
  104. package/dist/runstore/stageExecution.js +22 -0
  105. package/dist/runstore/stageSnapshot.d.ts +2 -0
  106. package/dist/runstore/stageSnapshot.js +46 -0
  107. package/dist/runstore/syntheticStageSnapshot.d.ts +2 -0
  108. package/dist/runstore/syntheticStageSnapshot.js +10 -0
  109. package/dist/runstore/trackProjection.d.ts +7 -0
  110. package/dist/runstore/trackProjection.js +94 -0
  111. package/dist/runstore/workspaceLayout.d.ts +31 -0
  112. package/dist/runstore/workspaceLayout.js +172 -0
  113. package/dist/runtime/answerResume.d.ts +16 -0
  114. package/dist/runtime/answerResume.js +22 -0
  115. package/dist/runtime/credentialBinding.d.ts +16 -0
  116. package/dist/runtime/credentialBinding.js +80 -0
  117. package/dist/runtime/dagTraversal.d.ts +3 -0
  118. package/dist/runtime/dagTraversal.js +26 -0
  119. package/dist/runtime/envelopeRouting.d.ts +20 -0
  120. package/dist/runtime/envelopeRouting.js +59 -0
  121. package/dist/runtime/hitlSeams.d.ts +38 -0
  122. package/dist/runtime/hitlSeams.js +2 -0
  123. package/dist/runtime/pipelineRunner.d.ts +77 -0
  124. package/dist/runtime/pipelineRunner.js +141 -0
  125. package/dist/runtime/pipelineScheduler.d.ts +90 -0
  126. package/dist/runtime/pipelineScheduler.js +494 -0
  127. package/dist/runtime/pipelineValidationError.d.ts +6 -0
  128. package/dist/runtime/pipelineValidationError.js +8 -0
  129. package/dist/runtime/resumeReconstruct.d.ts +33 -0
  130. package/dist/runtime/resumeReconstruct.js +144 -0
  131. package/dist/runtime/runManager.d.ts +129 -0
  132. package/dist/runtime/runManager.js +818 -0
  133. package/dist/runtime/runRetryCoordinator.d.ts +85 -0
  134. package/dist/runtime/runRetryCoordinator.js +372 -0
  135. package/dist/runtime/settingsFile.d.ts +17 -0
  136. package/dist/runtime/settingsFile.js +88 -0
  137. package/dist/runtime/stageAttemptBootstrap.d.ts +40 -0
  138. package/dist/runtime/stageAttemptBootstrap.js +82 -0
  139. package/dist/runtime/stageAttemptContext.d.ts +11 -0
  140. package/dist/runtime/stageAttemptContext.js +26 -0
  141. package/dist/runtime/stageConcurrency.d.ts +10 -0
  142. package/dist/runtime/stageConcurrency.js +51 -0
  143. package/dist/runtime/stageHitl.d.ts +95 -0
  144. package/dist/runtime/stageHitl.js +198 -0
  145. package/dist/runtime/stageProcessLauncher.d.ts +45 -0
  146. package/dist/runtime/stageProcessLauncher.js +199 -0
  147. package/dist/runtime/stageRecovery.d.ts +10 -0
  148. package/dist/runtime/stageRecovery.js +43 -0
  149. package/dist/runtime/stageRoots.d.ts +22 -0
  150. package/dist/runtime/stageRoots.js +93 -0
  151. package/dist/runtime/stageRunner.d.ts +59 -0
  152. package/dist/runtime/stageRunner.js +221 -0
  153. package/dist/runtime/stageWorker.d.ts +6 -0
  154. package/dist/runtime/stageWorker.js +109 -0
  155. package/dist/runtime/stageWorkerProtocol.d.ts +27 -0
  156. package/dist/runtime/stageWorkerProtocol.js +16 -0
  157. package/dist/runtime/taskInput.d.ts +15 -0
  158. package/dist/runtime/taskInput.js +38 -0
  159. package/dist/server/http.d.ts +31 -0
  160. package/dist/server/http.js +561 -0
  161. package/dist/server/providerRoutes.d.ts +15 -0
  162. package/dist/server/providerRoutes.js +186 -0
  163. package/dist/tools/askOperator.d.ts +185 -0
  164. package/dist/tools/askOperator.js +409 -0
  165. package/dist/tools/emitStageEnvelope.d.ts +42 -0
  166. package/dist/tools/emitStageEnvelope.js +61 -0
  167. package/dist/tools/writeStageArtifact.d.ts +26 -0
  168. package/dist/tools/writeStageArtifact.js +103 -0
  169. package/dist/types/envelope.d.ts +12 -0
  170. package/dist/types/envelope.js +6 -0
  171. package/dist/types/pipeline.d.ts +25 -0
  172. package/dist/types/pipeline.js +1 -0
  173. package/dist/types/stage.d.ts +12 -0
  174. package/dist/types/stage.js +6 -0
  175. package/dist/types/task.d.ts +7 -0
  176. package/dist/types/task.js +1 -0
  177. package/dist/ui/assets/index-Cry0Tpfx.js +118 -0
  178. package/dist/ui/assets/index-DCsDopak.css +1 -0
  179. package/dist/ui/index.html +21 -0
  180. package/package.json +72 -0
@@ -0,0 +1,967 @@
1
+ /**
2
+ * Pi implementation of AgentPort.
3
+ *
4
+ * One stage = one `createAgentSession` (single-shot SDK path, not
5
+ * AgentSessionRuntime / interactive / RPC). The session is sealed from host
6
+ * and global Pi resources so the stage YAML system prompt is the only
7
+ * instruction source. Operator credentials come from
8
+ * `ModelRuntime.create({ authPath })` using the factory-resolved binding
9
+ * (`pi_home` or `.stageflow/agent/auth.json`).
10
+ *
11
+ * Extension-backed providers (e.g. Cursor via pi-cursor-sdk) plug in through
12
+ * `StageProviderSupport` — see `providerSupport.ts` / `cursorProvider.ts`.
13
+ * Built-in providers (anthropic, openai, …) need no support module: sealed
14
+ * session + resolveCliModel after bindExtensions is enough.
15
+ *
16
+ * Lifecycle: create persisted session under the stage workspace →
17
+ * bindExtensions → setModel → `prompt()` → bounded abort + `dispose()` in
18
+ * finally (session file is kept). Stage completion is signaled only by
19
+ * `emit_stage_envelope` returning `terminate: true` (do not abort from
20
+ * inside the tool — that deadlocks bridges that wait on the tool result).
21
+ *
22
+ * HITL: `openStage` wires `AskOperatorWaitChannel` into a real StageHandle
23
+ * (tool Promise → wait yield → deliverAnswer). Same-process prefers the live
24
+ * channel; after process loss, open arms wait then `deliverAnswer` injects
25
+ * via `injectOpaqueAnswerIntoSession` and continues. Missing/corrupt session
26
+ * fails closed with `StageSessionReconstructError` (KTD7). Epic proving
27
+ * vehicle: pipeline `plan-review-proving` (Pi live wait + console answers).
28
+ *
29
+ * `ask_operator` is always registered on sealed stage sessions (allowlist +
30
+ * customTools).
31
+ */
32
+ import { existsSync } from "node:fs";
33
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
34
+ import path from "node:path";
35
+ import { createAgentSession, DefaultResourceLoader, defineTool, ModelRuntime, resolveCliModel, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
36
+ import { isAdvancingEnvelope } from "../envelope/check.js";
37
+ import { formatPriorEnvelope } from "../prompt/priorEnvelope.js";
38
+ import { attemptContext, noAttemptContext, } from "../runtime/stageAttemptContext.js";
39
+ import { createEmitStageEnvelopeTool, } from "../tools/emitStageEnvelope.js";
40
+ import { createAskOperatorTool, } from "../tools/askOperator.js";
41
+ import { createWriteStageArtifactTool } from "../tools/writeStageArtifact.js";
42
+ import "./cursorProvider.js";
43
+ import { findProviderSupport } from "./providerSupport.js";
44
+ import { mapSessionEventToActivity } from "./activity.js";
45
+ import { createStageActivityObserver } from "./activityObserver.js";
46
+ import { DEFAULT_STAGE_TIMEOUT_MS, runStageViaOpen } from "./port.js";
47
+ /**
48
+ * Stage tool allowlist for sealed Pi sessions.
49
+ * Always includes `ask_operator` (R7); `write_stage_artifact` when registered
50
+ * (always, for bound and unbound — bound/unbound only gates cwd / env bind).
51
+ */
52
+ export function resolveStageToolNames(emitToolName, artifactToolName, askOperatorToolName = "ask_operator") {
53
+ const tools = [
54
+ "read",
55
+ "bash",
56
+ "write",
57
+ "edit",
58
+ emitToolName,
59
+ askOperatorToolName,
60
+ ];
61
+ if (artifactToolName) {
62
+ tools.push(artifactToolName);
63
+ }
64
+ return tools;
65
+ }
66
+ /**
67
+ * In-process wait channel for `ask_operator` → StageHandle yield/deliver.
68
+ * HITL `openStage` connects this via `setWaitHandler` + handle `deliverAnswer`.
69
+ */
70
+ export class AskOperatorWaitChannel {
71
+ pending;
72
+ onWait;
73
+ setWaitHandler(handler) {
74
+ this.onWait = handler;
75
+ }
76
+ requestWait(prompt) {
77
+ const handler = this.onWait;
78
+ if (!handler) {
79
+ return Promise.reject(new Error("ask_operator: Pi live wait channel not connected (openStage must setWaitHandler; use FakeAgent for scripted HITL)"));
80
+ }
81
+ if (this.pending) {
82
+ return Promise.reject(new Error("ask_operator: wait already pending for this stage session"));
83
+ }
84
+ return new Promise((resolve, reject) => {
85
+ this.pending = { prompt, resolve, reject };
86
+ try {
87
+ handler(prompt);
88
+ }
89
+ catch (err) {
90
+ this.pending = undefined;
91
+ reject(err);
92
+ }
93
+ });
94
+ }
95
+ deliverAnswer(answer) {
96
+ if (!this.pending)
97
+ return false;
98
+ const { resolve } = this.pending;
99
+ this.pending = undefined;
100
+ resolve(answer);
101
+ return true;
102
+ }
103
+ rejectPending(reason) {
104
+ if (!this.pending)
105
+ return;
106
+ const { reject } = this.pending;
107
+ this.pending = undefined;
108
+ reject(new Error(reason));
109
+ }
110
+ get hasPending() {
111
+ return this.pending !== undefined;
112
+ }
113
+ }
114
+ /**
115
+ * Bridge used when no StageHandle wait loop is connected.
116
+ * Calling ask_operator fails closed instead of hanging the session.
117
+ */
118
+ export function createUnconnectedAskWaitBridge() {
119
+ return {
120
+ requestWait: async () => {
121
+ throw new Error("ask_operator: Pi live wait channel not connected (openStage must setWaitHandler; use FakeAgent for scripted HITL)");
122
+ },
123
+ };
124
+ }
125
+ /**
126
+ * Connect `AskOperatorWaitChannel` (tool Promise) to a StageHandle wait/deliver
127
+ * loop. Pipeline park stays on StageHitlController via this handle only —
128
+ * runtime never imports the channel.
129
+ */
130
+ export function createConnectedAskWaitStageHandle(options) {
131
+ const { askWaitChannel: channel } = options;
132
+ let closed = false;
133
+ let runLaunched = false;
134
+ let resumeArmed = options.resume !== undefined;
135
+ let resumeResolve;
136
+ let resumePromise;
137
+ let resumeDeliverError;
138
+ let resumeDeliverStarted = false;
139
+ if (resumeArmed) {
140
+ resumePromise = new Promise((resolve) => {
141
+ resumeResolve = resolve;
142
+ });
143
+ }
144
+ const eventQueue = [];
145
+ const eventWaiters = [];
146
+ const enqueue = (event) => {
147
+ const waiter = eventWaiters.shift();
148
+ if (waiter) {
149
+ waiter(event);
150
+ return;
151
+ }
152
+ eventQueue.push(event);
153
+ };
154
+ const takeEvent = () => {
155
+ if (eventQueue.length > 0) {
156
+ return Promise.resolve(eventQueue.shift());
157
+ }
158
+ return new Promise((resolve) => {
159
+ eventWaiters.push(resolve);
160
+ });
161
+ };
162
+ const settleFromRun = (run) => {
163
+ void run()
164
+ .then((result) => {
165
+ if (!closed) {
166
+ enqueue({ status: "completed", result });
167
+ }
168
+ })
169
+ .catch((err) => {
170
+ if (!closed) {
171
+ enqueue({
172
+ status: "completed",
173
+ result: {
174
+ ok: false,
175
+ reason: err instanceof Error ? err.message : String(err),
176
+ },
177
+ });
178
+ }
179
+ });
180
+ };
181
+ channel.setWaitHandler((prompt) => {
182
+ options.onBeforeWaitYield?.(prompt);
183
+ enqueue({ status: "waiting_for_input", request: prompt });
184
+ });
185
+ return {
186
+ stageId: options.stageId,
187
+ async next() {
188
+ if (closed) {
189
+ return {
190
+ status: "completed",
191
+ result: { ok: false, reason: "stage handle closed" },
192
+ };
193
+ }
194
+ if (resumeArmed) {
195
+ await resumePromise;
196
+ resumeArmed = false;
197
+ if (closed) {
198
+ return {
199
+ status: "completed",
200
+ result: { ok: false, reason: "stage handle closed" },
201
+ };
202
+ }
203
+ if (resumeDeliverError !== undefined) {
204
+ return {
205
+ status: "completed",
206
+ result: {
207
+ ok: false,
208
+ reason: resumeDeliverError instanceof Error
209
+ ? resumeDeliverError.message
210
+ : String(resumeDeliverError),
211
+ },
212
+ };
213
+ }
214
+ settleFromRun(options.resume.continueRun);
215
+ return takeEvent();
216
+ }
217
+ if (!runLaunched) {
218
+ runLaunched = true;
219
+ settleFromRun(options.run);
220
+ }
221
+ return takeEvent();
222
+ },
223
+ deliverAnswer(answer) {
224
+ if (resumeArmed && options.resume && !resumeDeliverStarted) {
225
+ resumeDeliverStarted = true;
226
+ try {
227
+ options.resume.onDeliver(answer);
228
+ }
229
+ catch (err) {
230
+ resumeDeliverError = err;
231
+ }
232
+ const resolve = resumeResolve;
233
+ resumeResolve = undefined;
234
+ resolve?.(answer);
235
+ return;
236
+ }
237
+ channel.deliverAnswer(answer);
238
+ },
239
+ async close(closeOptions) {
240
+ closed = true;
241
+ if (closeOptions?.park) {
242
+ await options.onClose?.({ park: true });
243
+ return;
244
+ }
245
+ channel.rejectPending("stage handle closed");
246
+ if (resumeResolve) {
247
+ const resolve = resumeResolve;
248
+ resumeResolve = undefined;
249
+ resumeArmed = false;
250
+ resolve(undefined);
251
+ }
252
+ while (eventWaiters.length > 0) {
253
+ const waiter = eventWaiters.shift();
254
+ waiter?.({
255
+ status: "completed",
256
+ result: { ok: false, reason: "stage handle closed" },
257
+ });
258
+ }
259
+ await options.onClose?.({ park: false });
260
+ },
261
+ };
262
+ }
263
+ /**
264
+ * Map Pi session events to StageActivityEvent at the adapter edge, then
265
+ * forward to the activity observer (stderr + onActivity).
266
+ */
267
+ function attachStageProgress(session, onActivity) {
268
+ const observer = createStageActivityObserver({ onActivity, writeStderr: true });
269
+ const unsubscribe = session.subscribe((event) => {
270
+ if (event.type === "message_update" &&
271
+ event.assistantMessageEvent.type === "text_delta") {
272
+ observer.onAssistantTextDelta(event.assistantMessageEvent.delta ?? "");
273
+ return;
274
+ }
275
+ if (event.type === "message_end" || event.type === "agent_end") {
276
+ observer.onStreamBoundary();
277
+ }
278
+ if (event.type === "agent_start" ||
279
+ event.type === "turn_start" ||
280
+ event.type === "tool_execution_start") {
281
+ observer.onStreamBoundary();
282
+ }
283
+ const activity = mapSessionEventToActivity(event);
284
+ if (!activity) {
285
+ return;
286
+ }
287
+ observer.onActivity(activity);
288
+ });
289
+ return () => {
290
+ unsubscribe();
291
+ observer.dispose();
292
+ };
293
+ }
294
+ function buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName) {
295
+ let emitHint = emitHintOverride ??
296
+ `Required final action: call ${emitToolName} exactly once with status, summary, and artifacts (run-relative paths).`;
297
+ if (input.stage.payload_schema !== undefined) {
298
+ emitHint += `\nOn status=success, payload is required and must match this JSON Schema:\n${JSON.stringify(input.stage.payload_schema, null, 2)}`;
299
+ }
300
+ const attempt = input.roots.attempt ?? 1;
301
+ const attemptArtifactsPath = `stages/${input.stage.id}/attempts/${attempt}/artifacts/`;
302
+ const skillBaseDir = input.stage.skill !== undefined && input.skillFilePath !== undefined
303
+ ? path.dirname(input.skillFilePath)
304
+ : undefined;
305
+ const artifactGuidance = input.roots.mode === "bound" && artifactToolName !== undefined
306
+ ? [
307
+ `Project checkout (agent cwd): ${input.roots.checkoutRoot}`,
308
+ `Factory run workspace: ${input.roots.runWorkspaceDir}`,
309
+ `Builtin tools (read, bash, write, edit) are for work inside the project checkout.`,
310
+ skillBaseDir !== undefined
311
+ ? `Absolute read and bash against ${skillBaseDir} are in bounds for the linked skill.`
312
+ : undefined,
313
+ `Create factory stage artifacts only with ${artifactToolName} (path relative to ${attemptArtifactsPath}). Use the returned run-relative path in ${emitToolName}.`,
314
+ `This is soft guidance: prefer ${artifactToolName} for factory files under the run workspace; do not rely on relative builtin writes for those files.`,
315
+ ]
316
+ .filter(Boolean)
317
+ .join("\n")
318
+ : artifactToolName !== undefined
319
+ ? [
320
+ `Create factory stage artifacts with ${artifactToolName} (path relative to ${attemptArtifactsPath}). Use the returned run-relative path in ${emitToolName}.`,
321
+ `This is soft guidance: prefer ${artifactToolName} for factory files under the run workspace; do not rely on relative builtin writes for those files.`,
322
+ ].join("\n")
323
+ : `Create factory stage artifacts under ${attemptArtifactsPath} relative to the run folder.`;
324
+ return [
325
+ `Task id: ${input.task.id}`,
326
+ `Stage id: ${input.stage.id}`,
327
+ `Goal: ${input.task.goal}`,
328
+ input.task.context ? `Context: ${input.task.context}` : "",
329
+ input.task.constraints ? `Constraints: ${input.task.constraints}` : "",
330
+ formatPriorEnvelope(input.priorEnvelope),
331
+ "",
332
+ artifactGuidance,
333
+ emitHint,
334
+ ]
335
+ .filter(Boolean)
336
+ .join("\n");
337
+ }
338
+ export function composeStageUserPrompt(input, emitToolName, emitHintOverride, artifactToolName) {
339
+ const body = buildUserPrompt(input, emitToolName, emitHintOverride, artifactToolName);
340
+ if (input.stage.skill !== undefined) {
341
+ return `/skill:${input.stage.skill} ${body}`;
342
+ }
343
+ return body;
344
+ }
345
+ /**
346
+ * DefaultResourceLoader with host/global discovery turned off.
347
+ *
348
+ * Without these flags the loader walks up from the run folder and would pick
349
+ * up the consumer project's AGENTS.md, `.agents/skills/`, `.pi/extensions`,
350
+ * and APPEND_SYSTEM.md. Stages must not inherit that context.
351
+ *
352
+ * `additionalExtensionPaths` is the only way extensions enter a sealed stage
353
+ * (used by StageProviderSupport implementations). With `noExtensions: true`,
354
+ * discovered global/project packages stay out; only allowlisted paths load.
355
+ * `additionalSkillPaths` is the matching allowlist for one named skill.
356
+ */
357
+ export function createSealedResourceLoader(options) {
358
+ return new DefaultResourceLoader({
359
+ cwd: options.cwd,
360
+ agentDir: options.agentDir,
361
+ settingsManager: options.settingsManager,
362
+ systemPromptOverride: () => options.systemPrompt,
363
+ appendSystemPromptOverride: () => [],
364
+ additionalExtensionPaths: options.additionalExtensionPaths,
365
+ additionalSkillPaths: options.additionalSkillPaths,
366
+ noContextFiles: true,
367
+ noExtensions: true,
368
+ noSkills: true,
369
+ noPromptTemplates: true,
370
+ noThemes: true,
371
+ });
372
+ }
373
+ async function shutdownSession(session) {
374
+ if (!session) {
375
+ return;
376
+ }
377
+ // Bound abort: waitForIdle can hang if a provider bridge is mid-flight.
378
+ try {
379
+ await Promise.race([
380
+ session.abort(),
381
+ new Promise((resolve) => setTimeout(resolve, 3_000)),
382
+ ]);
383
+ }
384
+ catch {
385
+ // ignore abort errors during teardown
386
+ }
387
+ try {
388
+ session.dispose();
389
+ }
390
+ catch {
391
+ // ignore dispose errors during teardown
392
+ }
393
+ }
394
+ function resultFromCapture(capture) {
395
+ if (capture.error && !capture.envelope) {
396
+ return { ok: false, reason: capture.error };
397
+ }
398
+ if (!capture.envelope) {
399
+ return { ok: false, reason: "missing emit_stage_envelope" };
400
+ }
401
+ if (!isAdvancingEnvelope(capture.envelope)) {
402
+ return {
403
+ ok: false,
404
+ reason: "status: failure",
405
+ envelope: capture.envelope,
406
+ };
407
+ }
408
+ return { ok: true, envelope: capture.envelope };
409
+ }
410
+ async function fileExists(filePath) {
411
+ try {
412
+ await access(filePath);
413
+ return true;
414
+ }
415
+ catch {
416
+ return false;
417
+ }
418
+ }
419
+ /** Stable Pi session JSONL path under the run workspace stage dir. */
420
+ export function stageSessionFilePath(roots, stageId) {
421
+ const ctx = roots.attempt !== undefined
422
+ ? attemptContext(roots.attempt)
423
+ : noAttemptContext();
424
+ return ctx.sessionPath(roots.runWorkspaceDir, stageId);
425
+ }
426
+ export class StageSessionReconstructError extends Error {
427
+ stageId;
428
+ sessionFile;
429
+ constructor(message, options) {
430
+ super(message, options.cause !== undefined ? { cause: options.cause } : undefined);
431
+ this.name = "StageSessionReconstructError";
432
+ this.stageId = options.stageId;
433
+ this.sessionFile = options.sessionFile;
434
+ }
435
+ }
436
+ /**
437
+ * Create or reopen the durable stage session file under the run workspace.
438
+ * Touches an empty file first so Pi flushes the session header immediately.
439
+ */
440
+ export async function createStageSessionManager(roots, stageId) {
441
+ const sessionFile = stageSessionFilePath(roots, stageId);
442
+ await mkdir(path.dirname(sessionFile), { recursive: true });
443
+ if (!(await fileExists(sessionFile))) {
444
+ await writeFile(sessionFile, "");
445
+ }
446
+ return SessionManager.open(sessionFile, path.dirname(sessionFile), roots.cwd);
447
+ }
448
+ /**
449
+ * Open an existing stage session for post-restart reconstruct.
450
+ * Missing or corrupt files fail closed (KTD7) — never create a fresh session.
451
+ */
452
+ export async function openStageSessionManager(roots, stageId) {
453
+ const sessionFile = stageSessionFilePath(roots, stageId);
454
+ if (!(await fileExists(sessionFile))) {
455
+ throw new StageSessionReconstructError(`stage session file missing for resume: ${sessionFile}`, { stageId, sessionFile });
456
+ }
457
+ try {
458
+ return SessionManager.open(sessionFile, path.dirname(sessionFile), roots.cwd);
459
+ }
460
+ catch (err) {
461
+ throw new StageSessionReconstructError(`stage session file corrupt or unreadable for resume: ${sessionFile}`, { stageId, sessionFile, cause: err });
462
+ }
463
+ }
464
+ /**
465
+ * Ensure the session is on disk before signaling waiting_for_input (KTD4).
466
+ * Dispose of the live AgentSession must not delete this file.
467
+ */
468
+ export function ensureStageSessionFlushed(sessionManager, stageId) {
469
+ const sessionFile = sessionManager.getSessionFile();
470
+ if (!sessionManager.isPersisted() || !sessionFile) {
471
+ throw new StageSessionReconstructError("stage session is not persisted; cannot wait for input", { stageId, sessionFile: sessionFile ?? "(none)" });
472
+ }
473
+ return sessionFile;
474
+ }
475
+ const PREMATURE_ASK_OPERATOR_CLOSE = "stage handle closed";
476
+ /**
477
+ * Remove an erroneous ask_operator toolResult written when a parked worker
478
+ * tears down before the operator answers. Restores an open tool call on disk.
479
+ */
480
+ export async function repairPrematureAskOperatorClosure(sessionFile) {
481
+ let text;
482
+ try {
483
+ text = await readFile(sessionFile, "utf8");
484
+ }
485
+ catch {
486
+ return false;
487
+ }
488
+ const lines = text.split("\n").filter((line) => line.trim().length > 0);
489
+ if (lines.length === 0) {
490
+ return false;
491
+ }
492
+ const entries = [];
493
+ for (const line of lines) {
494
+ try {
495
+ entries.push(JSON.parse(line));
496
+ }
497
+ catch {
498
+ return false;
499
+ }
500
+ }
501
+ let changed = false;
502
+ for (let i = entries.length - 1; i >= 0; i--) {
503
+ const entry = entries[i];
504
+ if (!entry || typeof entry !== "object")
505
+ continue;
506
+ if (entry.type !== "message")
507
+ continue;
508
+ const message = entry.message;
509
+ if (!message || message.role !== "toolResult")
510
+ continue;
511
+ if (message.toolName !== "ask_operator" || message.isError !== true)
512
+ continue;
513
+ const content = message.content;
514
+ if (!Array.isArray(content))
515
+ continue;
516
+ const first = content[0];
517
+ if (first?.text !== PREMATURE_ASK_OPERATOR_CLOSE)
518
+ continue;
519
+ entries.splice(i, 1);
520
+ changed = true;
521
+ while (i < entries.length) {
522
+ const next = entries[i];
523
+ if (!next || typeof next !== "object")
524
+ break;
525
+ if (next.type !== "message")
526
+ break;
527
+ const nextMessage = next.message;
528
+ if (nextMessage?.role !== "assistant")
529
+ break;
530
+ if (nextMessage.stopReason === "error" ||
531
+ typeof nextMessage.errorMessage === "string") {
532
+ entries.splice(i, 1);
533
+ continue;
534
+ }
535
+ break;
536
+ }
537
+ break;
538
+ }
539
+ if (!changed) {
540
+ return false;
541
+ }
542
+ await writeFile(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`);
543
+ return true;
544
+ }
545
+ function opaqueAnswerToText(answer) {
546
+ if (typeof answer === "string")
547
+ return answer;
548
+ try {
549
+ return JSON.stringify(answer);
550
+ }
551
+ catch {
552
+ return String(answer);
553
+ }
554
+ }
555
+ function findOpenToolCall(sessionManager) {
556
+ const entries = sessionManager.buildContextEntries();
557
+ const answered = new Set();
558
+ for (const entry of entries) {
559
+ if (entry.type !== "message")
560
+ continue;
561
+ const message = entry.message;
562
+ if (message.role === "toolResult" && typeof message.toolCallId === "string") {
563
+ answered.add(message.toolCallId);
564
+ }
565
+ }
566
+ for (let i = entries.length - 1; i >= 0; i--) {
567
+ const entry = entries[i];
568
+ if (entry.type !== "message")
569
+ continue;
570
+ const message = entry.message;
571
+ if (message.role !== "assistant" || !Array.isArray(message.content))
572
+ continue;
573
+ for (const block of message.content) {
574
+ if (block &&
575
+ typeof block === "object" &&
576
+ block.type === "toolCall") {
577
+ const toolCall = block;
578
+ if (typeof toolCall.id === "string" &&
579
+ typeof toolCall.name === "string" &&
580
+ !answered.has(toolCall.id)) {
581
+ return { toolCallId: toolCall.id, toolName: toolCall.name };
582
+ }
583
+ }
584
+ }
585
+ }
586
+ return undefined;
587
+ }
588
+ /**
589
+ * Inject an opaque operator answer into the persisted conversation (R9).
590
+ * Prefers completing an open tool call as a tool result (T2 seam); otherwise
591
+ * appends a custom message that stays in LLM context.
592
+ */
593
+ export function injectOpaqueAnswerIntoSession(sessionManager, answer) {
594
+ const text = opaqueAnswerToText(answer);
595
+ const open = findOpenToolCall(sessionManager);
596
+ if (open) {
597
+ sessionManager.appendMessage({
598
+ role: "toolResult",
599
+ toolCallId: open.toolCallId,
600
+ toolName: open.toolName,
601
+ content: [{ type: "text", text }],
602
+ isError: false,
603
+ timestamp: Date.now(),
604
+ });
605
+ return {
606
+ injectedAs: "tool_result",
607
+ toolCallId: open.toolCallId,
608
+ toolName: open.toolName,
609
+ };
610
+ }
611
+ sessionManager.appendCustomMessageEntry("stageflow.operator_answer", text, true, { answer });
612
+ return { injectedAs: "custom_message" };
613
+ }
614
+ async function prepareStageSessionWiring(input, sessionManager, existingAskWaitChannel) {
615
+ const { roots } = input;
616
+ const provider = findProviderSupport(input.stage.model);
617
+ const capture = {};
618
+ const askWaitChannel = existingAskWaitChannel ?? new AskOperatorWaitChannel();
619
+ const emitDef = createEmitStageEnvelopeTool(capture, input.stage.payload_schema);
620
+ const emitTool = defineTool(emitDef);
621
+ const askDef = createAskOperatorTool({
622
+ requestWait: (prompt) => askWaitChannel.requestWait(prompt),
623
+ });
624
+ const askTool = defineTool(askDef);
625
+ const artifactDef = createWriteStageArtifactTool({
626
+ runWorkspaceDir: roots.runWorkspaceDir,
627
+ stageId: input.stage.id,
628
+ attempt: roots.attempt ?? 1,
629
+ });
630
+ const artifactTool = defineTool(artifactDef);
631
+ if (input.stage.skill !== undefined && input.skillFilePath === undefined) {
632
+ return {
633
+ ok: false,
634
+ reason: `Skill "${input.stage.skill}" is not installed`,
635
+ };
636
+ }
637
+ const additionalExtensionPaths = [];
638
+ let restoreProvider;
639
+ if (provider) {
640
+ const prepared = provider.prepare(input.stage.model);
641
+ if (prepared.error) {
642
+ return { ok: false, reason: prepared.error };
643
+ }
644
+ additionalExtensionPaths.push(...prepared.extensionPaths);
645
+ restoreProvider = prepared.restore;
646
+ }
647
+ try {
648
+ const modelRuntime = await ModelRuntime.create(roots.authPath ? { authPath: roots.authPath } : undefined);
649
+ const settingsManager = SettingsManager.inMemory({
650
+ compaction: { enabled: false },
651
+ });
652
+ const loader = createSealedResourceLoader({
653
+ cwd: roots.cwd,
654
+ agentDir: roots.agentDir,
655
+ settingsManager,
656
+ systemPrompt: input.stage.system_prompt,
657
+ additionalExtensionPaths,
658
+ ...(input.skillFilePath !== undefined
659
+ ? { additionalSkillPaths: [input.skillFilePath] }
660
+ : {}),
661
+ });
662
+ await loader.reload();
663
+ const extensionErrors = loader.getExtensions().errors;
664
+ if (extensionErrors.length > 0) {
665
+ restoreProvider?.();
666
+ return {
667
+ ok: false,
668
+ reason: `Failed to load Pi extension(s): ${extensionErrors
669
+ .map((e) => `${e.path}: ${e.error}`)
670
+ .join("; ")}`,
671
+ };
672
+ }
673
+ const skillDiagnostics = loader
674
+ .getSkills()
675
+ .diagnostics.filter((d) => d.type === "error" || d.type === "collision");
676
+ if (skillDiagnostics.length > 0) {
677
+ restoreProvider?.();
678
+ return {
679
+ ok: false,
680
+ reason: `Failed to load skill(s): ${skillDiagnostics
681
+ .map((d) => (d.path !== undefined ? `${d.path}: ${d.message}` : d.message))
682
+ .join("; ")}`,
683
+ };
684
+ }
685
+ if (input.stage.skill !== undefined &&
686
+ !loader.getSkills().skills.some((skill) => skill.name === input.stage.skill)) {
687
+ restoreProvider?.();
688
+ return {
689
+ ok: false,
690
+ reason: `Skill "${input.stage.skill}" is not installed`,
691
+ };
692
+ }
693
+ const customTools = [emitTool, askTool, artifactTool];
694
+ return {
695
+ sessionManager,
696
+ modelRuntime,
697
+ settingsManager,
698
+ loader,
699
+ tools: resolveStageToolNames(emitDef.name, artifactDef.name, askDef.name),
700
+ customTools,
701
+ emitDefName: emitDef.name,
702
+ askOperatorDefName: askDef.name,
703
+ artifactDefName: artifactDef.name,
704
+ providerEmitHint: provider?.emitToolHint?.(emitDef.name),
705
+ restoreProvider,
706
+ capture,
707
+ askWaitChannel,
708
+ };
709
+ }
710
+ catch (err) {
711
+ restoreProvider?.();
712
+ throw err;
713
+ }
714
+ }
715
+ function isStageRunResult(value) {
716
+ return "ok" in value;
717
+ }
718
+ /**
719
+ * Reconstruct an equivalent Pi session after process loss and inject the
720
+ * operator answer into conversation context (R5, R9, R10 / KTD5).
721
+ * Does not auto-continue the model turn — U4 owns the yield loop.
722
+ */
723
+ export async function reconstructStageSessionForAnswer(input, answer) {
724
+ const sessionManager = await openStageSessionManager(input.roots, input.stage.id);
725
+ const sessionFile = ensureStageSessionFlushed(sessionManager, input.stage.id);
726
+ let injection;
727
+ try {
728
+ injection = injectOpaqueAnswerIntoSession(sessionManager, answer);
729
+ }
730
+ catch (err) {
731
+ throw new StageSessionReconstructError(`failed to inject answer into stage session: ${sessionFile}`, { stageId: input.stage.id, sessionFile, cause: err });
732
+ }
733
+ const wiring = await prepareStageSessionWiring(input, sessionManager);
734
+ if (isStageRunResult(wiring)) {
735
+ throw new StageSessionReconstructError(wiring.ok === false ? wiring.reason : "stage session reconstruct failed", { stageId: input.stage.id, sessionFile });
736
+ }
737
+ let session;
738
+ try {
739
+ const created = await createAgentSession({
740
+ cwd: input.roots.cwd,
741
+ agentDir: input.roots.agentDir,
742
+ modelRuntime: wiring.modelRuntime,
743
+ tools: wiring.tools,
744
+ customTools: wiring.customTools,
745
+ resourceLoader: wiring.loader,
746
+ sessionManager,
747
+ settingsManager: wiring.settingsManager,
748
+ });
749
+ session = created.session;
750
+ await session.bindExtensions({});
751
+ const resolved = resolveCliModel({
752
+ cliModel: input.stage.model,
753
+ modelRuntime: wiring.modelRuntime,
754
+ });
755
+ if (resolved.error || !resolved.model) {
756
+ throw new StageSessionReconstructError(resolved.error ?? `Model not found: ${input.stage.model}`, { stageId: input.stage.id, sessionFile });
757
+ }
758
+ await session.setModel(resolved.model);
759
+ if (resolved.thinkingLevel) {
760
+ session.setThinkingLevel(resolved.thinkingLevel);
761
+ }
762
+ const liveSession = session;
763
+ return {
764
+ session: liveSession,
765
+ sessionManager,
766
+ sessionFile,
767
+ injection,
768
+ restore: () => {
769
+ wiring.restoreProvider?.();
770
+ },
771
+ shutdown: async () => {
772
+ await shutdownSession(liveSession);
773
+ wiring.restoreProvider?.();
774
+ },
775
+ };
776
+ }
777
+ catch (err) {
778
+ await shutdownSession(session);
779
+ wiring.restoreProvider?.();
780
+ if (err instanceof StageSessionReconstructError)
781
+ throw err;
782
+ throw new StageSessionReconstructError(`failed to reconstruct stage session: ${err instanceof Error ? err.message : String(err)}`, { stageId: input.stage.id, sessionFile, cause: err });
783
+ }
784
+ }
785
+ async function bindStageSession(input, sessionManager, wiring) {
786
+ const { roots } = input;
787
+ const created = await createAgentSession({
788
+ cwd: roots.cwd,
789
+ agentDir: roots.agentDir,
790
+ modelRuntime: wiring.modelRuntime,
791
+ tools: wiring.tools,
792
+ customTools: wiring.customTools,
793
+ resourceLoader: wiring.loader,
794
+ sessionManager,
795
+ settingsManager: wiring.settingsManager,
796
+ });
797
+ const session = created.session;
798
+ ensureStageSessionFlushed(sessionManager, input.stage.id);
799
+ await session.bindExtensions({});
800
+ const resolved = resolveCliModel({
801
+ cliModel: input.stage.model,
802
+ modelRuntime: wiring.modelRuntime,
803
+ });
804
+ if (resolved.error || !resolved.model) {
805
+ await shutdownSession(session);
806
+ return {
807
+ ok: false,
808
+ reason: resolved.error ?? `Model not found: ${input.stage.model}`,
809
+ };
810
+ }
811
+ try {
812
+ await session.setModel(resolved.model);
813
+ if (resolved.thinkingLevel) {
814
+ session.setThinkingLevel(resolved.thinkingLevel);
815
+ }
816
+ }
817
+ catch (err) {
818
+ await shutdownSession(session);
819
+ return {
820
+ ok: false,
821
+ reason: err instanceof Error ? err.message : String(err),
822
+ };
823
+ }
824
+ return session;
825
+ }
826
+ function syncAgentMessagesFromSession(session, sessionManager) {
827
+ session.agent.state.messages = sessionManager.buildSessionContext().messages;
828
+ }
829
+ export class PiAgentAdapter {
830
+ openStage(input) {
831
+ const timeoutMs = input.timeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
832
+ const askWaitChannel = new AskOperatorWaitChannel();
833
+ const sessionFile = input.resumeToken ?? stageSessionFilePath(input.roots, input.stage.id);
834
+ let sessionManager;
835
+ let resumeWaiting = false;
836
+ if (existsSync(sessionFile)) {
837
+ try {
838
+ sessionManager = SessionManager.open(sessionFile, path.dirname(sessionFile), input.roots.cwd);
839
+ }
840
+ catch (err) {
841
+ throw new StageSessionReconstructError(`stage session file corrupt or unreadable for resume: ${sessionFile}`, {
842
+ stageId: input.stage.id,
843
+ sessionFile,
844
+ cause: err,
845
+ });
846
+ }
847
+ ensureStageSessionFlushed(sessionManager, input.stage.id);
848
+ if (!findOpenToolCall(sessionManager)) {
849
+ throw new StageSessionReconstructError(`stage session has no open tool call for resume: ${sessionFile}`, { stageId: input.stage.id, sessionFile });
850
+ }
851
+ resumeWaiting = true;
852
+ }
853
+ let session;
854
+ let wiring;
855
+ let unsubscribeProgress;
856
+ let prepareError;
857
+ const preparePromise = (async () => {
858
+ if (!sessionManager) {
859
+ sessionManager = await createStageSessionManager(input.roots, input.stage.id);
860
+ }
861
+ const prepared = await prepareStageSessionWiring(input, sessionManager, askWaitChannel);
862
+ if (isStageRunResult(prepared)) {
863
+ prepareError = prepared;
864
+ return;
865
+ }
866
+ wiring = prepared;
867
+ const bound = await bindStageSession(input, sessionManager, wiring);
868
+ if (bound && typeof bound === "object" && "ok" in bound) {
869
+ prepareError = bound;
870
+ wiring.restoreProvider?.();
871
+ wiring = undefined;
872
+ return;
873
+ }
874
+ session = bound;
875
+ unsubscribeProgress = attachStageProgress(session, input.onActivity);
876
+ })();
877
+ const runWithTimeout = async (work) => {
878
+ await preparePromise;
879
+ if (prepareError)
880
+ return prepareError;
881
+ if (!session || !wiring || !sessionManager) {
882
+ return { ok: false, reason: "stage session failed to prepare" };
883
+ }
884
+ const controller = new AbortController();
885
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
886
+ try {
887
+ const workPromise = work();
888
+ const abortPromise = new Promise((_, reject) => {
889
+ controller.signal.addEventListener("abort", () => {
890
+ reject(new Error(`stage timed out after ${timeoutMs}ms`));
891
+ });
892
+ });
893
+ await Promise.race([workPromise, abortPromise]);
894
+ return resultFromCapture(wiring.capture);
895
+ }
896
+ catch (err) {
897
+ if (wiring.capture.envelope && isAdvancingEnvelope(wiring.capture.envelope)) {
898
+ return { ok: true, envelope: wiring.capture.envelope };
899
+ }
900
+ return {
901
+ ok: false,
902
+ reason: err instanceof Error ? err.message : String(err),
903
+ envelope: wiring.capture.envelope,
904
+ };
905
+ }
906
+ finally {
907
+ clearTimeout(timer);
908
+ }
909
+ };
910
+ return createConnectedAskWaitStageHandle({
911
+ stageId: input.stage.id,
912
+ askWaitChannel,
913
+ onBeforeWaitYield: () => {
914
+ if (sessionManager) {
915
+ ensureStageSessionFlushed(sessionManager, input.stage.id);
916
+ }
917
+ },
918
+ run: async () => {
919
+ return runWithTimeout(async () => {
920
+ const userPrompt = composeStageUserPrompt(input, wiring.emitDefName, wiring.providerEmitHint, wiring.artifactDefName);
921
+ await session.prompt(userPrompt);
922
+ });
923
+ },
924
+ resume: resumeWaiting
925
+ ? {
926
+ onDeliver: (answer) => {
927
+ if (!sessionManager) {
928
+ throw new StageSessionReconstructError("stage session missing during resume deliver", { stageId: input.stage.id, sessionFile });
929
+ }
930
+ injectOpaqueAnswerIntoSession(sessionManager, answer);
931
+ if (session) {
932
+ syncAgentMessagesFromSession(session, sessionManager);
933
+ }
934
+ },
935
+ continueRun: async () => {
936
+ return runWithTimeout(async () => {
937
+ if (!session || !sessionManager) {
938
+ throw new StageSessionReconstructError("stage session missing during resume continue", { stageId: input.stage.id, sessionFile });
939
+ }
940
+ syncAgentMessagesFromSession(session, sessionManager);
941
+ await session.agent.continue();
942
+ });
943
+ },
944
+ }
945
+ : undefined,
946
+ onClose: async (closeOptions) => {
947
+ await preparePromise.catch(() => undefined);
948
+ if (closeOptions?.park) {
949
+ if (sessionManager) {
950
+ ensureStageSessionFlushed(sessionManager, input.stage.id);
951
+ if (askWaitChannel.hasPending) {
952
+ await repairPrematureAskOperatorClosure(sessionFile);
953
+ }
954
+ }
955
+ return;
956
+ }
957
+ askWaitChannel.rejectPending(PREMATURE_ASK_OPERATOR_CLOSE);
958
+ unsubscribeProgress?.();
959
+ await shutdownSession(session);
960
+ wiring?.restoreProvider?.();
961
+ },
962
+ });
963
+ }
964
+ async runStage(input) {
965
+ return runStageViaOpen(this, input);
966
+ }
967
+ }