@tt-a1i/openpi 0.4.0 → 0.5.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.
- package/README.md +102 -40
- package/SETUP.md +22 -6
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +13 -5
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/render.ts +17 -257
- package/extensions/file-search/src/binaries.ts +57 -41
- package/extensions/git-read/index.ts +1 -3
- package/extensions/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +153 -86
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +123 -127
- package/extensions/shared/activity-status.ts +30 -0
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/capability-intent.ts +1 -1
- package/extensions/shared/child-session.ts +437 -21
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +73 -33
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +72 -15
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +29 -2
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +270 -59
- package/extensions/subagents/navigation.ts +34 -5
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/manager.ts +34 -2
- package/extensions/subagents/src/prompt.ts +32 -4
- package/extensions/subagents/src/result-artifact.ts +4 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +15 -1
- package/extensions/subagents/src/ui/takeover.ts +73 -257
- package/extensions/subagents/src/ui/transcript.ts +38 -535
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/ui-customization/footer.ts +0 -40
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +1 -1
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +167 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +488 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +74 -4
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +66 -6
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +128 -64
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +25 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +1 -1
- package/skills/workflows/REFERENCE.md +4 -2
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -303
|
@@ -24,7 +24,11 @@ import {
|
|
|
24
24
|
import type { Cause, Scope } from "effect";
|
|
25
25
|
import { Effect, Queue, Stream } from "effect";
|
|
26
26
|
import { resolveAgentModel } from "../agent-types.ts";
|
|
27
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
SubagentBackend,
|
|
29
|
+
SubagentCleanupReceipt,
|
|
30
|
+
SubagentSession,
|
|
31
|
+
} from "../backend.ts";
|
|
28
32
|
import type {
|
|
29
33
|
SpawnTask,
|
|
30
34
|
SubagentEvent,
|
|
@@ -40,9 +44,14 @@ import {
|
|
|
40
44
|
createChildResources,
|
|
41
45
|
shutdownAndDisposeChildSession,
|
|
42
46
|
} from "../../../shared/child-session.ts";
|
|
43
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
formatWorktreeCleanupWarning,
|
|
49
|
+
reclaimWorktree,
|
|
50
|
+
} from "../../../shared/worktree.ts";
|
|
51
|
+
import { AgentToolRenderLedger } from "../../../shared/agent-tool-renderer.ts";
|
|
44
52
|
|
|
45
53
|
const DIRECT_WORKTREE_CLEANUP_TIMEOUT_MS = 4_000;
|
|
54
|
+
const PARTIAL_TEXT_MAX_LENGTH = 128 * 1_024;
|
|
46
55
|
|
|
47
56
|
// --- Model + effort resolution -----------------------------------------------
|
|
48
57
|
|
|
@@ -50,6 +59,19 @@ type ThinkingLevel = NonNullable<
|
|
|
50
59
|
NonNullable<Parameters<typeof createAgentSession>[0]>["thinkingLevel"]
|
|
51
60
|
>;
|
|
52
61
|
|
|
62
|
+
export type PiAgentSessionFactory = (
|
|
63
|
+
options: Parameters<typeof createAgentSession>[0],
|
|
64
|
+
) => Promise<{ session: AgentSession }>;
|
|
65
|
+
|
|
66
|
+
export interface PiBackendOptions {
|
|
67
|
+
/** Test seam for production-adapter lifecycle coverage. */
|
|
68
|
+
readonly sessionFactory?: PiAgentSessionFactory;
|
|
69
|
+
/** Test-only override for bounded child shutdown. */
|
|
70
|
+
readonly shutdownTimeoutMs?: number;
|
|
71
|
+
/** Test seam for observing whether unsafe worktree reclamation is attempted. */
|
|
72
|
+
readonly worktreeCleanup?: typeof reclaimWorktree;
|
|
73
|
+
}
|
|
74
|
+
|
|
53
75
|
// --- Event translation ----------------------------------------------------------
|
|
54
76
|
|
|
55
77
|
function messageRole(msg: unknown): Message["role"] | undefined {
|
|
@@ -70,20 +92,12 @@ function lastAssistantMessage(
|
|
|
70
92
|
return undefined;
|
|
71
93
|
}
|
|
72
94
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const text = (msg as AssistantMessage).content
|
|
80
|
-
.filter((part) => part.type === "text")
|
|
81
|
-
.map((part) => part.text)
|
|
82
|
-
.join("\n")
|
|
83
|
-
.trim();
|
|
84
|
-
if (text) return text;
|
|
85
|
-
}
|
|
86
|
-
return "";
|
|
95
|
+
function assistantText(message: AssistantMessage) {
|
|
96
|
+
return message.content
|
|
97
|
+
.filter((part) => part.type === "text")
|
|
98
|
+
.map((part) => part.text)
|
|
99
|
+
.join("\n")
|
|
100
|
+
.trim();
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
function safeJson(value: unknown): string | undefined {
|
|
@@ -165,6 +179,7 @@ function boundedError(error: unknown) {
|
|
|
165
179
|
|
|
166
180
|
const makePiSession = (
|
|
167
181
|
task: SpawnTask,
|
|
182
|
+
options: PiBackendOptions,
|
|
168
183
|
): Effect.Effect<SubagentSession, SpawnError, Scope.Scope> =>
|
|
169
184
|
Effect.gen(function* () {
|
|
170
185
|
const registry = task.parent.modelRegistry;
|
|
@@ -192,7 +207,9 @@ const makePiSession = (
|
|
|
192
207
|
? { appendSystemPrompt: [...task.appendSystemPrompt] }
|
|
193
208
|
: {}),
|
|
194
209
|
});
|
|
195
|
-
const { session } = await
|
|
210
|
+
const { session } = await (
|
|
211
|
+
options.sessionFactory ?? createAgentSession
|
|
212
|
+
)({
|
|
196
213
|
cwd: task.cwd,
|
|
197
214
|
sessionManager: SessionManager.create(task.cwd),
|
|
198
215
|
settingsManager,
|
|
@@ -207,7 +224,9 @@ const makePiSession = (
|
|
|
207
224
|
try {
|
|
208
225
|
await bindChildSessionExtensions(session, task.tools);
|
|
209
226
|
} catch (error) {
|
|
210
|
-
await shutdownAndDisposeChildSession(session
|
|
227
|
+
await shutdownAndDisposeChildSession(session, {
|
|
228
|
+
timeoutMs: options.shutdownTimeoutMs,
|
|
229
|
+
});
|
|
211
230
|
throw error;
|
|
212
231
|
}
|
|
213
232
|
return session;
|
|
@@ -215,14 +234,35 @@ const makePiSession = (
|
|
|
215
234
|
catch: (error) => new SpawnError({ message: boundedError(error) }),
|
|
216
235
|
});
|
|
217
236
|
|
|
237
|
+
interface ActivePrompt {
|
|
238
|
+
cancelled: boolean;
|
|
239
|
+
lifecycleStarted: boolean;
|
|
240
|
+
lifecycleSettled: boolean;
|
|
241
|
+
promptSettled: boolean;
|
|
242
|
+
promptError?: string;
|
|
243
|
+
lastAssistant?: AssistantMessage;
|
|
244
|
+
finalText: string;
|
|
245
|
+
liveText: string;
|
|
246
|
+
partialTextAtCancel?: string;
|
|
247
|
+
promise: Promise<void>;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
interface PendingRestart {
|
|
251
|
+
cancelled: boolean;
|
|
252
|
+
terminalEmitted: boolean;
|
|
253
|
+
}
|
|
254
|
+
|
|
218
255
|
const state = {
|
|
219
256
|
closed: false,
|
|
220
|
-
/** prompt() rejection for the active run; folded into RunSettled. */
|
|
221
|
-
runError: undefined as string | undefined,
|
|
222
257
|
/** One terminal event per run: lifecycle, prompt-rejection, and abort
|
|
223
258
|
* fallbacks can all race to settle; the first wins. */
|
|
224
259
|
settled: false,
|
|
260
|
+
/** Pi events have no run identity, so the exact prompt Promise is the
|
|
261
|
+
* only safe lease for deciding when another run may start. */
|
|
262
|
+
activePrompt: undefined as ActivePrompt | undefined,
|
|
263
|
+
pendingRestart: undefined as PendingRestart | undefined,
|
|
225
264
|
};
|
|
265
|
+
let cleanupReceipt: SubagentCleanupReceipt | undefined;
|
|
226
266
|
|
|
227
267
|
const events = yield* Queue.make<SubagentEvent, Cause.Done>();
|
|
228
268
|
const emit = (event: SubagentEvent) => {
|
|
@@ -231,6 +271,7 @@ const makePiSession = (
|
|
|
231
271
|
|
|
232
272
|
const toolTimeout = createToolCallTimeoutGuard();
|
|
233
273
|
toolTimeout.apply(session);
|
|
274
|
+
const toolRenderer = new AgentToolRenderLedger();
|
|
234
275
|
|
|
235
276
|
const activeModel = (): Model<any> | undefined => {
|
|
236
277
|
const sessionModel = session.model;
|
|
@@ -269,20 +310,13 @@ const makePiSession = (
|
|
|
269
310
|
});
|
|
270
311
|
};
|
|
271
312
|
|
|
272
|
-
const settle = () => {
|
|
273
|
-
if (state.settled) return;
|
|
313
|
+
const settle = (prompt = state.activePrompt) => {
|
|
314
|
+
if (!prompt || state.activePrompt !== prompt || state.settled) return;
|
|
274
315
|
state.settled = true;
|
|
275
|
-
const last =
|
|
276
|
-
const partialText =
|
|
277
|
-
if (last?.stopReason === "aborted") {
|
|
278
|
-
emit({
|
|
279
|
-
_tag: "RunSettled",
|
|
280
|
-
outcome: { _tag: "Interrupted", partialText },
|
|
281
|
-
});
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
316
|
+
const last = prompt.lastAssistant;
|
|
317
|
+
const partialText = prompt.liveText || prompt.finalText || undefined;
|
|
284
318
|
const errorText =
|
|
285
|
-
|
|
319
|
+
prompt.promptError ??
|
|
286
320
|
(last?.stopReason === "error"
|
|
287
321
|
? (last.errorMessage ?? "Run failed")
|
|
288
322
|
: undefined);
|
|
@@ -297,24 +331,86 @@ const makePiSession = (
|
|
|
297
331
|
});
|
|
298
332
|
return;
|
|
299
333
|
}
|
|
334
|
+
if (last?.stopReason === "aborted") {
|
|
335
|
+
emit({
|
|
336
|
+
_tag: "RunSettled",
|
|
337
|
+
outcome: { _tag: "Interrupted", partialText },
|
|
338
|
+
});
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
300
341
|
emit({
|
|
301
342
|
_tag: "RunSettled",
|
|
302
|
-
outcome: {
|
|
343
|
+
outcome: {
|
|
344
|
+
_tag: "Completed",
|
|
345
|
+
finalText: prompt.finalText,
|
|
346
|
+
},
|
|
303
347
|
});
|
|
304
348
|
};
|
|
305
349
|
|
|
350
|
+
/**
|
|
351
|
+
* A Pi lifecycle event is evidence, not the lease itself. The exact
|
|
352
|
+
* prompt() Promise must also settle before a terminal outcome is trusted;
|
|
353
|
+
* otherwise agent_settled can publish Completed and hide a later reject.
|
|
354
|
+
*/
|
|
355
|
+
const maybeSettle = (prompt: ActivePrompt) => {
|
|
356
|
+
if (
|
|
357
|
+
state.closed ||
|
|
358
|
+
state.activePrompt !== prompt ||
|
|
359
|
+
prompt.cancelled ||
|
|
360
|
+
state.settled ||
|
|
361
|
+
!prompt.promptSettled
|
|
362
|
+
) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (prompt.promptError !== undefined) {
|
|
366
|
+
settle(prompt);
|
|
367
|
+
} else if (!prompt.lifecycleStarted) {
|
|
368
|
+
prompt.promptError =
|
|
369
|
+
"Pi prompt completed without lifecycle events; no agent response was observed.";
|
|
370
|
+
settle(prompt);
|
|
371
|
+
} else if (!prompt.lifecycleSettled) {
|
|
372
|
+
prompt.promptError =
|
|
373
|
+
"Pi prompt completed without an agent_settled lifecycle event.";
|
|
374
|
+
settle(prompt);
|
|
375
|
+
} else {
|
|
376
|
+
settle(prompt);
|
|
377
|
+
}
|
|
378
|
+
if (state.activePrompt === prompt && prompt.promptSettled) {
|
|
379
|
+
state.activePrompt = undefined;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
306
383
|
const handleEvent = (event: AgentSessionEvent) => {
|
|
307
384
|
if (state.closed) return;
|
|
385
|
+
const activePrompt = state.activePrompt;
|
|
386
|
+
if (!activePrompt) return;
|
|
387
|
+
if (activePrompt.cancelled) {
|
|
388
|
+
if (event.type === "agent_start") {
|
|
389
|
+
// abort() can return during Pi's preflight while prompt() remains
|
|
390
|
+
// pending. If that preflight later enters the agent lifecycle,
|
|
391
|
+
// abort again before it can reach a provider or tool. Never reopen
|
|
392
|
+
// the event gate: native events carry no generation identity.
|
|
393
|
+
try {
|
|
394
|
+
void session.abort().catch(() => undefined);
|
|
395
|
+
} catch {
|
|
396
|
+
// The original interrupt still owns bounded cleanup.
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
308
401
|
switch (event.type) {
|
|
309
402
|
case "agent_start":
|
|
310
403
|
// Extensions may register tools between runs; guard new ones too.
|
|
311
404
|
toolTimeout.apply(session);
|
|
312
|
-
|
|
405
|
+
activePrompt.lifecycleStarted = true;
|
|
313
406
|
emit({ _tag: "RunStarted" });
|
|
314
407
|
break;
|
|
315
408
|
case "message_update": {
|
|
316
409
|
const streamEvent = event.assistantMessageEvent;
|
|
317
410
|
if (streamEvent.type === "text_delta") {
|
|
411
|
+
activePrompt.liveText = (
|
|
412
|
+
activePrompt.liveText + streamEvent.delta
|
|
413
|
+
).slice(-PARTIAL_TEXT_MAX_LENGTH);
|
|
318
414
|
emit({
|
|
319
415
|
_tag: "AssistantDelta",
|
|
320
416
|
kind: "text",
|
|
@@ -335,9 +431,14 @@ const makePiSession = (
|
|
|
335
431
|
const text = userText(event.message as Message);
|
|
336
432
|
if (text.trim()) emit({ _tag: "UserMessage", text });
|
|
337
433
|
} else if (role === "assistant") {
|
|
434
|
+
const assistant = event.message as AssistantMessage;
|
|
435
|
+
activePrompt.lastAssistant = assistant;
|
|
436
|
+
const text = assistantText(assistant);
|
|
437
|
+
if (text) activePrompt.finalText = text;
|
|
438
|
+
activePrompt.liveText = "";
|
|
338
439
|
emit({
|
|
339
440
|
_tag: "AssistantMessage",
|
|
340
|
-
parts: assistantParts(
|
|
441
|
+
parts: assistantParts(assistant),
|
|
341
442
|
});
|
|
342
443
|
emitUsage();
|
|
343
444
|
emit({ _tag: "MetaChanged", meta: currentMeta() });
|
|
@@ -346,6 +447,12 @@ const makePiSession = (
|
|
|
346
447
|
break;
|
|
347
448
|
}
|
|
348
449
|
case "tool_execution_start":
|
|
450
|
+
toolRenderer.start(
|
|
451
|
+
event.toolCallId,
|
|
452
|
+
event.toolName,
|
|
453
|
+
event.args,
|
|
454
|
+
session.getToolDefinition(event.toolName),
|
|
455
|
+
);
|
|
349
456
|
emit({
|
|
350
457
|
_tag: "ToolStart",
|
|
351
458
|
toolId: event.toolCallId,
|
|
@@ -354,6 +461,12 @@ const makePiSession = (
|
|
|
354
461
|
});
|
|
355
462
|
break;
|
|
356
463
|
case "tool_execution_update":
|
|
464
|
+
toolRenderer.update(
|
|
465
|
+
event.toolCallId,
|
|
466
|
+
event.toolName,
|
|
467
|
+
event.args,
|
|
468
|
+
event.partialResult,
|
|
469
|
+
);
|
|
357
470
|
emit({
|
|
358
471
|
_tag: "ToolUpdate",
|
|
359
472
|
toolId: event.toolCallId,
|
|
@@ -361,6 +474,12 @@ const makePiSession = (
|
|
|
361
474
|
});
|
|
362
475
|
break;
|
|
363
476
|
case "tool_execution_end":
|
|
477
|
+
toolRenderer.end(
|
|
478
|
+
event.toolCallId,
|
|
479
|
+
event.toolName,
|
|
480
|
+
event.result,
|
|
481
|
+
event.isError,
|
|
482
|
+
);
|
|
364
483
|
emit({
|
|
365
484
|
_tag: "ToolEnd",
|
|
366
485
|
toolId: event.toolCallId,
|
|
@@ -385,7 +504,8 @@ const makePiSession = (
|
|
|
385
504
|
});
|
|
386
505
|
break;
|
|
387
506
|
case "agent_settled":
|
|
388
|
-
|
|
507
|
+
activePrompt.lifecycleSettled = true;
|
|
508
|
+
maybeSettle(activePrompt);
|
|
389
509
|
break;
|
|
390
510
|
}
|
|
391
511
|
};
|
|
@@ -400,17 +520,61 @@ const makePiSession = (
|
|
|
400
520
|
} catch {
|
|
401
521
|
// Continue with abort/dispose.
|
|
402
522
|
}
|
|
403
|
-
await shutdownAndDisposeChildSession(session, {
|
|
523
|
+
const shutdown = await shutdownAndDisposeChildSession(session, {
|
|
404
524
|
abort: true,
|
|
405
|
-
timeoutMs: CHILD_SHUTDOWN_TIMEOUT_MS,
|
|
525
|
+
timeoutMs: options.shutdownTimeoutMs ?? CHILD_SHUTDOWN_TIMEOUT_MS,
|
|
406
526
|
});
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
527
|
+
const promptQuiescent =
|
|
528
|
+
state.activePrompt === undefined || state.activePrompt.promptSettled;
|
|
529
|
+
const cleanupMessages = [...shutdown.errors];
|
|
530
|
+
let cleanupUncertain =
|
|
531
|
+
shutdown.errors.length > 0 || shutdown.timedOut || !promptQuiescent;
|
|
532
|
+
if (shutdown.timedOut)
|
|
533
|
+
cleanupMessages.push("cleanup deadline exceeded");
|
|
534
|
+
if (!promptQuiescent) {
|
|
535
|
+
cleanupMessages.push(
|
|
536
|
+
"active prompt did not quiesce before session shutdown",
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// A preflight hook can outlive abort() and continue touching the child
|
|
541
|
+
// cwd. Never reclaim a worktree until both the prompt and session
|
|
542
|
+
// cleanup have reached a known-safe terminal state.
|
|
543
|
+
if (
|
|
544
|
+
task.worktree &&
|
|
545
|
+
promptQuiescent &&
|
|
546
|
+
shutdown.ok &&
|
|
547
|
+
!shutdown.timedOut
|
|
548
|
+
) {
|
|
549
|
+
try {
|
|
550
|
+
const cleanup = await (options.worktreeCleanup ?? reclaimWorktree)(
|
|
551
|
+
task.worktree.repoCwd,
|
|
552
|
+
task.worktree,
|
|
553
|
+
{
|
|
554
|
+
timeoutMs: DIRECT_WORKTREE_CLEANUP_TIMEOUT_MS,
|
|
555
|
+
},
|
|
556
|
+
);
|
|
557
|
+
const warning = formatWorktreeCleanupWarning(
|
|
558
|
+
cleanup,
|
|
559
|
+
task.worktree.path,
|
|
560
|
+
);
|
|
561
|
+
if (warning) cleanupMessages.push(`worktree cleanup: ${warning}`);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
cleanupUncertain = true;
|
|
564
|
+
cleanupMessages.push(
|
|
565
|
+
`worktree cleanup failed: ${boundedError(error)}; checkout preserved at ${task.worktree.path}`,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
} else if (task.worktree) {
|
|
569
|
+
cleanupMessages.push(
|
|
570
|
+
`worktree cleanup skipped; checkout preserved at ${task.worktree.path}`,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
if (cleanupMessages.length > 0) {
|
|
574
|
+
cleanupReceipt = {
|
|
575
|
+
uncertain: cleanupUncertain,
|
|
576
|
+
message: `${cleanupUncertain ? "Subagent cleanup is uncertain" : "Subagent cleanup"}: ${cleanupMessages.join("; ")}`,
|
|
577
|
+
};
|
|
414
578
|
}
|
|
415
579
|
Queue.endUnsafe(events);
|
|
416
580
|
}),
|
|
@@ -418,15 +582,63 @@ const makePiSession = (
|
|
|
418
582
|
|
|
419
583
|
/** Start a fresh run (v1 manager.run): fire-and-forget, errors -> events. */
|
|
420
584
|
const startRun = (text: string) => {
|
|
421
|
-
state.
|
|
585
|
+
if (state.activePrompt) {
|
|
586
|
+
throw new Error(
|
|
587
|
+
"Cannot start a Pi prompt while another prompt is active.",
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
const activePrompt: ActivePrompt = {
|
|
591
|
+
cancelled: false,
|
|
592
|
+
lifecycleStarted: false,
|
|
593
|
+
lifecycleSettled: false,
|
|
594
|
+
promptSettled: false,
|
|
595
|
+
finalText: "",
|
|
596
|
+
liveText: "",
|
|
597
|
+
promise: Promise.resolve(),
|
|
598
|
+
};
|
|
599
|
+
state.activePrompt = activePrompt;
|
|
422
600
|
state.settled = false;
|
|
423
601
|
emit({ _tag: "RunStarted" });
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
602
|
+
let prompt: Promise<void>;
|
|
603
|
+
try {
|
|
604
|
+
prompt = session.prompt(text, {
|
|
605
|
+
preflightResult: (accepted) => {
|
|
606
|
+
if (accepted && (activePrompt.cancelled || state.closed)) {
|
|
607
|
+
// This callback runs immediately before Pi enters
|
|
608
|
+
// _runAgentPrompt. Throwing here is the only synchronous seam
|
|
609
|
+
// that prevents a cancelled preflight from reviving after the
|
|
610
|
+
// adapter subscription and worktree ownership are gone.
|
|
611
|
+
throw new Error(
|
|
612
|
+
"Subagent prompt was cancelled during preflight.",
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
} catch (error) {
|
|
618
|
+
prompt = Promise.reject(error);
|
|
619
|
+
}
|
|
620
|
+
activePrompt.promise = prompt
|
|
621
|
+
.then(
|
|
622
|
+
() => undefined,
|
|
623
|
+
(error) => {
|
|
624
|
+
if (
|
|
625
|
+
!state.closed &&
|
|
626
|
+
state.activePrompt === activePrompt &&
|
|
627
|
+
!activePrompt.cancelled
|
|
628
|
+
) {
|
|
629
|
+
activePrompt.promptError = boundedError(error);
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
)
|
|
633
|
+
.finally(() => {
|
|
634
|
+
activePrompt.promptSettled = true;
|
|
635
|
+
maybeSettle(activePrompt);
|
|
636
|
+
if (state.activePrompt === activePrompt) {
|
|
637
|
+
if (activePrompt.cancelled || state.settled) {
|
|
638
|
+
state.activePrompt = undefined;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
});
|
|
430
642
|
};
|
|
431
643
|
|
|
432
644
|
// Session naming is best-effort.
|
|
@@ -444,6 +656,8 @@ const makePiSession = (
|
|
|
444
656
|
startRun(task.prompt);
|
|
445
657
|
|
|
446
658
|
return {
|
|
659
|
+
toolRenderer,
|
|
660
|
+
cleanupReceipt: () => cleanupReceipt,
|
|
447
661
|
meta: Effect.sync(currentMeta),
|
|
448
662
|
events: Stream.fromQueue(events),
|
|
449
663
|
send: (text) =>
|
|
@@ -451,7 +665,21 @@ const makePiSession = (
|
|
|
451
665
|
if (state.closed) {
|
|
452
666
|
return new SendError({ message: "Subagent session is closed." });
|
|
453
667
|
}
|
|
454
|
-
|
|
668
|
+
const pendingRestart = state.pendingRestart;
|
|
669
|
+
if (pendingRestart) {
|
|
670
|
+
return new SendError({
|
|
671
|
+
message: pendingRestart.cancelled
|
|
672
|
+
? "Subagent cancellation is still in progress."
|
|
673
|
+
: "A subagent restart is already pending.",
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
const activePrompt = state.activePrompt;
|
|
677
|
+
if (activePrompt?.cancelled) {
|
|
678
|
+
return new SendError({
|
|
679
|
+
message: "Subagent cancellation is still in progress.",
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
if (activePrompt && session.isStreaming) {
|
|
455
683
|
// Steer the active run via the SDK's queue; queue_update events
|
|
456
684
|
// render it, message_end(user) lands it in the transcript. A
|
|
457
685
|
// rejected steer is a real send failure, not a diagnostic.
|
|
@@ -460,34 +688,115 @@ const makePiSession = (
|
|
|
460
688
|
catch: (error) => new SendError({ message: boundedError(error) }),
|
|
461
689
|
}).pipe(Effect.asVoid);
|
|
462
690
|
}
|
|
691
|
+
if (
|
|
692
|
+
activePrompt &&
|
|
693
|
+
!activePrompt.lifecycleSettled &&
|
|
694
|
+
!activePrompt.promptSettled
|
|
695
|
+
) {
|
|
696
|
+
return new SendError({
|
|
697
|
+
message:
|
|
698
|
+
"Subagent prompt is still starting; wait for it to run or settle before sending.",
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
if (activePrompt) {
|
|
702
|
+
// Pi emits agent_settled before prompt() resolves. Preserve the
|
|
703
|
+
// same-session restart contract while closing that microtask gap
|
|
704
|
+
// without ever overlapping two native prompts.
|
|
705
|
+
const restart: PendingRestart = {
|
|
706
|
+
cancelled: false,
|
|
707
|
+
terminalEmitted: false,
|
|
708
|
+
};
|
|
709
|
+
state.pendingRestart = restart;
|
|
710
|
+
return Effect.tryPromise({
|
|
711
|
+
try: async (signal) => {
|
|
712
|
+
const cancelPendingRestart = () => {
|
|
713
|
+
restart.cancelled = true;
|
|
714
|
+
if (state.pendingRestart === restart) {
|
|
715
|
+
state.pendingRestart = undefined;
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
signal.addEventListener("abort", cancelPendingRestart, {
|
|
719
|
+
once: true,
|
|
720
|
+
});
|
|
721
|
+
if (signal.aborted) cancelPendingRestart();
|
|
722
|
+
try {
|
|
723
|
+
await activePrompt.promise;
|
|
724
|
+
if (state.closed) {
|
|
725
|
+
throw new Error("Subagent session is closed.");
|
|
726
|
+
}
|
|
727
|
+
if (restart.cancelled) {
|
|
728
|
+
// interrupt emits the terminal event for this accepted
|
|
729
|
+
// restart. Resolve the send without starting a prompt so
|
|
730
|
+
// the manager keeps its restarting marker until that
|
|
731
|
+
// terminal event is folded.
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
state.pendingRestart = undefined;
|
|
735
|
+
startRun(text);
|
|
736
|
+
} finally {
|
|
737
|
+
signal.removeEventListener("abort", cancelPendingRestart);
|
|
738
|
+
if (state.pendingRestart === restart) {
|
|
739
|
+
state.pendingRestart = undefined;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
catch: (error) => new SendError({ message: boundedError(error) }),
|
|
744
|
+
}).pipe(Effect.asVoid);
|
|
745
|
+
}
|
|
463
746
|
return Effect.sync(() => startRun(text));
|
|
464
747
|
}),
|
|
465
748
|
interrupt: Effect.promise(async () => {
|
|
466
749
|
if (state.closed) return;
|
|
750
|
+
const pendingRestart = state.pendingRestart;
|
|
751
|
+
if (pendingRestart) pendingRestart.cancelled = true;
|
|
752
|
+
const activePrompt = state.activePrompt;
|
|
753
|
+
if (activePrompt) {
|
|
754
|
+
activePrompt.cancelled = true;
|
|
755
|
+
activePrompt.partialTextAtCancel = pendingRestart
|
|
756
|
+
? undefined
|
|
757
|
+
: activePrompt.liveText || activePrompt.finalText || undefined;
|
|
758
|
+
}
|
|
467
759
|
try {
|
|
468
760
|
session.clearQueue();
|
|
469
761
|
} catch {
|
|
470
762
|
// Abort regardless.
|
|
471
763
|
}
|
|
472
764
|
await session.abort().catch(() => undefined);
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
// manager
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
765
|
+
// isStreaming is false during Pi preflight, so it cannot prove the
|
|
766
|
+
// invocation is quiescent. Wait for the exact prompt Promise instead;
|
|
767
|
+
// the manager bounds this effect at 5s and force-disposes on timeout.
|
|
768
|
+
await activePrompt?.promise;
|
|
769
|
+
if (
|
|
770
|
+
!state.closed &&
|
|
771
|
+
pendingRestart &&
|
|
772
|
+
!pendingRestart.terminalEmitted
|
|
773
|
+
) {
|
|
774
|
+
pendingRestart.terminalEmitted = true;
|
|
775
|
+
if (state.pendingRestart === pendingRestart) {
|
|
776
|
+
state.pendingRestart = undefined;
|
|
777
|
+
}
|
|
778
|
+
state.settled = true;
|
|
779
|
+
emit({ _tag: "RunSettled", outcome: { _tag: "Interrupted" } });
|
|
780
|
+
return;
|
|
479
781
|
}
|
|
480
|
-
// No streaming run means no agent_settled will arrive; emit the
|
|
481
|
-
// terminal event (once) so the run cannot look running forever.
|
|
482
782
|
if (!state.closed && !state.settled) {
|
|
483
783
|
state.settled = true;
|
|
484
|
-
emit({
|
|
784
|
+
emit({
|
|
785
|
+
_tag: "RunSettled",
|
|
786
|
+
outcome: {
|
|
787
|
+
_tag: "Interrupted",
|
|
788
|
+
partialText: activePrompt?.partialTextAtCancel,
|
|
789
|
+
},
|
|
790
|
+
});
|
|
485
791
|
}
|
|
486
792
|
}),
|
|
487
793
|
} satisfies SubagentSession;
|
|
488
794
|
});
|
|
489
795
|
|
|
490
|
-
export const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
796
|
+
export const makePiBackend = (options: PiBackendOptions = {}) =>
|
|
797
|
+
({
|
|
798
|
+
name: "pi",
|
|
799
|
+
spawn: (task) => makePiSession(task, options),
|
|
800
|
+
}) satisfies SubagentBackend;
|
|
801
|
+
|
|
802
|
+
export const piBackend = makePiBackend();
|
|
@@ -31,6 +31,7 @@ export const REASONING_EFFORTS = [
|
|
|
31
31
|
export type ReasoningEffort = (typeof REASONING_EFFORTS)[number];
|
|
32
32
|
|
|
33
33
|
export type SubagentStatus = "running" | "done" | "error";
|
|
34
|
+
export type SubagentOutcome = "completed" | "failed" | "interrupted";
|
|
34
35
|
|
|
35
36
|
/** Parent-session context resolved by the tool layer and passed opaquely. */
|
|
36
37
|
export interface ParentContext {
|
|
@@ -215,12 +216,16 @@ export interface SubagentSnapshot {
|
|
|
215
216
|
readonly prompt: string;
|
|
216
217
|
readonly cwd: string;
|
|
217
218
|
readonly status: SubagentStatus;
|
|
219
|
+
readonly outcome?: SubagentOutcome;
|
|
220
|
+
readonly worktreeBranch?: string;
|
|
218
221
|
readonly createdAt: number;
|
|
219
222
|
readonly settledAt?: number;
|
|
220
223
|
readonly errorText?: string;
|
|
221
224
|
readonly meta: SubagentMeta;
|
|
222
225
|
readonly usage: { readonly tokens?: number; readonly contextWindow?: number };
|
|
223
226
|
readonly transcript: ReadonlyArray<TranscriptItem>;
|
|
227
|
+
/** Monotonic version bumped on every transcript mutation (see manager). */
|
|
228
|
+
readonly transcriptVersion: number;
|
|
224
229
|
/** Streaming assistant buffers, cleared when the finalized message lands. */
|
|
225
230
|
readonly liveAssistant?: { readonly text: string; readonly thinking: string };
|
|
226
231
|
readonly liveTools: ReadonlyArray<LiveToolState>;
|