@tt-a1i/openpi 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -46
- package/SETUP.md +29 -7
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +155 -0
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +59 -0
- package/extensions/ai-providers/antigravity/credentials.ts +52 -0
- package/extensions/ai-providers/antigravity/discovery.ts +130 -0
- package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
- package/extensions/ai-providers/antigravity/models.ts +84 -0
- package/extensions/ai-providers/antigravity/oauth.ts +700 -0
- package/extensions/ai-providers/antigravity/provider.ts +1116 -0
- package/extensions/ai-providers/antigravity/routing.ts +340 -0
- package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
- package/extensions/ai-providers/cursor/constants.ts +5 -0
- package/extensions/ai-providers/cursor/credentials.ts +14 -0
- package/extensions/ai-providers/cursor/discovery.ts +291 -0
- package/extensions/ai-providers/cursor/input-images.ts +106 -0
- package/extensions/ai-providers/cursor/models.ts +45 -0
- package/extensions/ai-providers/cursor/oauth.ts +263 -0
- package/extensions/ai-providers/cursor/proto.ts +1064 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
- package/extensions/ai-providers/cursor/provider.ts +1175 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
- package/extensions/ai-providers/index.ts +86 -0
- package/extensions/ai-providers/oauth-adapter.ts +81 -0
- package/extensions/ai-providers/usage.ts +10 -0
- package/extensions/background-terminals/index.ts +38 -3
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +484 -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 +47 -24
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +12 -2
- 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/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +65 -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 +82 -6
- 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 +138 -130
- 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 +457 -21
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +83 -34
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/structured-output.ts +154 -0
- 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 +313 -62
- package/extensions/subagents/navigation.ts +34 -5
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +450 -70
- package/extensions/subagents/src/domain.ts +21 -1
- package/extensions/subagents/src/manager.ts +39 -2
- package/extensions/subagents/src/prompt.ts +49 -7
- package/extensions/subagents/src/result-artifact.ts +36 -0
- package/extensions/subagents/src/result-delivery.ts +39 -14
- 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 +16 -45
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +257 -0
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +459 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +175 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +501 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +79 -5
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +70 -16
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +214 -76
- package/extensions/workflows/retention.ts +599 -0
- package/extensions/workflows/runner.ts +389 -345
- 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 +34 -14
- package/skills/subagents/REFERENCE.md +190 -0
- package/skills/subagents/SKILL.md +2 -1
- package/skills/workflows/REFERENCE.md +6 -4
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +664 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +1069 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +467 -0
- package/web/runtime/pi-runtime.ts +1206 -0
- package/web/runtime/types.ts +102 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1700 -0
- package/web/ui/index.html +142 -0
- package/web/ui/styles.css +680 -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
|
@@ -19,12 +19,17 @@ import type {
|
|
|
19
19
|
} from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import {
|
|
21
21
|
createAgentSession,
|
|
22
|
+
getAgentDir,
|
|
22
23
|
SessionManager,
|
|
23
24
|
} from "@earendil-works/pi-coding-agent";
|
|
24
25
|
import type { Cause, Scope } from "effect";
|
|
25
26
|
import { Effect, Queue, Stream } from "effect";
|
|
26
27
|
import { resolveAgentModel } from "../agent-types.ts";
|
|
27
|
-
import type {
|
|
28
|
+
import type {
|
|
29
|
+
SubagentBackend,
|
|
30
|
+
SubagentCleanupReceipt,
|
|
31
|
+
SubagentSession,
|
|
32
|
+
} from "../backend.ts";
|
|
28
33
|
import type {
|
|
29
34
|
SpawnTask,
|
|
30
35
|
SubagentEvent,
|
|
@@ -40,9 +45,22 @@ import {
|
|
|
40
45
|
createChildResources,
|
|
41
46
|
shutdownAndDisposeChildSession,
|
|
42
47
|
} from "../../../shared/child-session.ts";
|
|
43
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
formatWorktreeCleanupWarning,
|
|
50
|
+
reclaimWorktree,
|
|
51
|
+
} from "../../../shared/worktree.ts";
|
|
52
|
+
import { AgentToolRenderLedger } from "../../../shared/agent-tool-renderer.ts";
|
|
53
|
+
import {
|
|
54
|
+
childToolsWithStructuredOutput,
|
|
55
|
+
createStructuredOutputTool,
|
|
56
|
+
encodeStructuredResult,
|
|
57
|
+
type EncodedStructuredResult,
|
|
58
|
+
STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION,
|
|
59
|
+
} from "../../../shared/structured-output.ts";
|
|
60
|
+
import { persistStructuredResultArtifact } from "../result-artifact.ts";
|
|
44
61
|
|
|
45
62
|
const DIRECT_WORKTREE_CLEANUP_TIMEOUT_MS = 4_000;
|
|
63
|
+
const PARTIAL_TEXT_MAX_LENGTH = 128 * 1_024;
|
|
46
64
|
|
|
47
65
|
// --- Model + effort resolution -----------------------------------------------
|
|
48
66
|
|
|
@@ -50,6 +68,19 @@ type ThinkingLevel = NonNullable<
|
|
|
50
68
|
NonNullable<Parameters<typeof createAgentSession>[0]>["thinkingLevel"]
|
|
51
69
|
>;
|
|
52
70
|
|
|
71
|
+
export type PiAgentSessionFactory = (
|
|
72
|
+
options: Parameters<typeof createAgentSession>[0],
|
|
73
|
+
) => Promise<{ session: AgentSession }>;
|
|
74
|
+
|
|
75
|
+
export interface PiBackendOptions {
|
|
76
|
+
/** Test seam for production-adapter lifecycle coverage. */
|
|
77
|
+
readonly sessionFactory?: PiAgentSessionFactory;
|
|
78
|
+
/** Test-only override for bounded child shutdown. */
|
|
79
|
+
readonly shutdownTimeoutMs?: number;
|
|
80
|
+
/** Test seam for observing whether unsafe worktree reclamation is attempted. */
|
|
81
|
+
readonly worktreeCleanup?: typeof reclaimWorktree;
|
|
82
|
+
}
|
|
83
|
+
|
|
53
84
|
// --- Event translation ----------------------------------------------------------
|
|
54
85
|
|
|
55
86
|
function messageRole(msg: unknown): Message["role"] | undefined {
|
|
@@ -70,20 +101,12 @@ function lastAssistantMessage(
|
|
|
70
101
|
return undefined;
|
|
71
102
|
}
|
|
72
103
|
|
|
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 "";
|
|
104
|
+
function assistantText(message: AssistantMessage) {
|
|
105
|
+
return message.content
|
|
106
|
+
.filter((part) => part.type === "text")
|
|
107
|
+
.map((part) => part.text)
|
|
108
|
+
.join("\n")
|
|
109
|
+
.trim();
|
|
87
110
|
}
|
|
88
111
|
|
|
89
112
|
function safeJson(value: unknown): string | undefined {
|
|
@@ -165,6 +188,7 @@ function boundedError(error: unknown) {
|
|
|
165
188
|
|
|
166
189
|
const makePiSession = (
|
|
167
190
|
task: SpawnTask,
|
|
191
|
+
options: PiBackendOptions,
|
|
168
192
|
): Effect.Effect<SubagentSession, SpawnError, Scope.Scope> =>
|
|
169
193
|
Effect.gen(function* () {
|
|
170
194
|
const registry = task.parent.modelRegistry;
|
|
@@ -183,31 +207,61 @@ const makePiSession = (
|
|
|
183
207
|
const thinkingLevel = (task.reasoningEffort ??
|
|
184
208
|
task.parent.inheritedThinkingLevel) as ThinkingLevel | undefined;
|
|
185
209
|
|
|
210
|
+
let capturedStructured: EncodedStructuredResult | undefined;
|
|
211
|
+
const structuredOutputTool =
|
|
212
|
+
task.outputSchema === undefined
|
|
213
|
+
? undefined
|
|
214
|
+
: createStructuredOutputTool(task.outputSchema, (value) => {
|
|
215
|
+
capturedStructured = encodeStructuredResult(value);
|
|
216
|
+
});
|
|
217
|
+
|
|
186
218
|
const session = yield* Effect.tryPromise({
|
|
187
219
|
try: async () => {
|
|
220
|
+
const appendSystemPrompt = [
|
|
221
|
+
...(task.appendSystemPrompt ?? []),
|
|
222
|
+
...(structuredOutputTool
|
|
223
|
+
? [STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION]
|
|
224
|
+
: []),
|
|
225
|
+
];
|
|
188
226
|
const { loader, settingsManager } = await createChildResources({
|
|
189
227
|
cwd: task.cwd,
|
|
190
228
|
projectTrusted: task.parent.projectTrusted,
|
|
191
|
-
...(
|
|
192
|
-
? { appendSystemPrompt: [...task.appendSystemPrompt] }
|
|
193
|
-
: {}),
|
|
229
|
+
...(appendSystemPrompt.length > 0 ? { appendSystemPrompt } : {}),
|
|
194
230
|
});
|
|
195
|
-
const { session } = await
|
|
231
|
+
const { session } = await (
|
|
232
|
+
options.sessionFactory ?? createAgentSession
|
|
233
|
+
)({
|
|
196
234
|
cwd: task.cwd,
|
|
197
235
|
sessionManager: SessionManager.create(task.cwd),
|
|
198
236
|
settingsManager,
|
|
199
237
|
resourceLoader: loader,
|
|
200
238
|
model,
|
|
201
239
|
thinkingLevel,
|
|
202
|
-
...
|
|
240
|
+
...(structuredOutputTool
|
|
241
|
+
? { customTools: [structuredOutputTool] }
|
|
242
|
+
: {}),
|
|
243
|
+
...childToolPolicy(
|
|
244
|
+
childToolsWithStructuredOutput(
|
|
245
|
+
task.tools,
|
|
246
|
+
structuredOutputTool !== undefined,
|
|
247
|
+
),
|
|
248
|
+
),
|
|
203
249
|
});
|
|
204
250
|
// Start child extension session hooks/resources in headless mode.
|
|
205
251
|
// A rejection here would otherwise leak the freshly created session:
|
|
206
252
|
// the scope finalizer that owns cleanup is only registered later.
|
|
207
253
|
try {
|
|
208
|
-
await bindChildSessionExtensions(
|
|
254
|
+
await bindChildSessionExtensions(
|
|
255
|
+
session,
|
|
256
|
+
childToolsWithStructuredOutput(
|
|
257
|
+
task.tools,
|
|
258
|
+
structuredOutputTool !== undefined,
|
|
259
|
+
),
|
|
260
|
+
);
|
|
209
261
|
} catch (error) {
|
|
210
|
-
await shutdownAndDisposeChildSession(session
|
|
262
|
+
await shutdownAndDisposeChildSession(session, {
|
|
263
|
+
timeoutMs: options.shutdownTimeoutMs,
|
|
264
|
+
});
|
|
211
265
|
throw error;
|
|
212
266
|
}
|
|
213
267
|
return session;
|
|
@@ -215,14 +269,35 @@ const makePiSession = (
|
|
|
215
269
|
catch: (error) => new SpawnError({ message: boundedError(error) }),
|
|
216
270
|
});
|
|
217
271
|
|
|
272
|
+
interface ActivePrompt {
|
|
273
|
+
cancelled: boolean;
|
|
274
|
+
lifecycleStarted: boolean;
|
|
275
|
+
lifecycleSettled: boolean;
|
|
276
|
+
promptSettled: boolean;
|
|
277
|
+
promptError?: string;
|
|
278
|
+
lastAssistant?: AssistantMessage;
|
|
279
|
+
finalText: string;
|
|
280
|
+
liveText: string;
|
|
281
|
+
partialTextAtCancel?: string;
|
|
282
|
+
promise: Promise<void>;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
interface PendingRestart {
|
|
286
|
+
cancelled: boolean;
|
|
287
|
+
terminalEmitted: boolean;
|
|
288
|
+
}
|
|
289
|
+
|
|
218
290
|
const state = {
|
|
219
291
|
closed: false,
|
|
220
|
-
/** prompt() rejection for the active run; folded into RunSettled. */
|
|
221
|
-
runError: undefined as string | undefined,
|
|
222
292
|
/** One terminal event per run: lifecycle, prompt-rejection, and abort
|
|
223
293
|
* fallbacks can all race to settle; the first wins. */
|
|
224
294
|
settled: false,
|
|
295
|
+
/** Pi events have no run identity, so the exact prompt Promise is the
|
|
296
|
+
* only safe lease for deciding when another run may start. */
|
|
297
|
+
activePrompt: undefined as ActivePrompt | undefined,
|
|
298
|
+
pendingRestart: undefined as PendingRestart | undefined,
|
|
225
299
|
};
|
|
300
|
+
let cleanupReceipt: SubagentCleanupReceipt | undefined;
|
|
226
301
|
|
|
227
302
|
const events = yield* Queue.make<SubagentEvent, Cause.Done>();
|
|
228
303
|
const emit = (event: SubagentEvent) => {
|
|
@@ -231,6 +306,7 @@ const makePiSession = (
|
|
|
231
306
|
|
|
232
307
|
const toolTimeout = createToolCallTimeoutGuard();
|
|
233
308
|
toolTimeout.apply(session);
|
|
309
|
+
const toolRenderer = new AgentToolRenderLedger();
|
|
234
310
|
|
|
235
311
|
const activeModel = (): Model<any> | undefined => {
|
|
236
312
|
const sessionModel = session.model;
|
|
@@ -269,11 +345,27 @@ const makePiSession = (
|
|
|
269
345
|
});
|
|
270
346
|
};
|
|
271
347
|
|
|
272
|
-
const settle = () => {
|
|
273
|
-
if (state.settled) return;
|
|
348
|
+
const settle = (prompt = state.activePrompt) => {
|
|
349
|
+
if (!prompt || state.activePrompt !== prompt || state.settled) return;
|
|
274
350
|
state.settled = true;
|
|
275
|
-
const last =
|
|
276
|
-
const partialText =
|
|
351
|
+
const last = prompt.lastAssistant;
|
|
352
|
+
const partialText = prompt.liveText || prompt.finalText || undefined;
|
|
353
|
+
const errorText =
|
|
354
|
+
prompt.promptError ??
|
|
355
|
+
(last?.stopReason === "error"
|
|
356
|
+
? (last.errorMessage ?? "Run failed")
|
|
357
|
+
: undefined);
|
|
358
|
+
if (errorText !== undefined) {
|
|
359
|
+
emit({
|
|
360
|
+
_tag: "RunSettled",
|
|
361
|
+
outcome: {
|
|
362
|
+
_tag: "Failed",
|
|
363
|
+
errorText: boundedError(errorText),
|
|
364
|
+
partialText,
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
277
369
|
if (last?.stopReason === "aborted") {
|
|
278
370
|
emit({
|
|
279
371
|
_tag: "RunSettled",
|
|
@@ -281,40 +373,114 @@ const makePiSession = (
|
|
|
281
373
|
});
|
|
282
374
|
return;
|
|
283
375
|
}
|
|
284
|
-
|
|
285
|
-
state.runError ??
|
|
286
|
-
(last?.stopReason === "error"
|
|
287
|
-
? (last.errorMessage ?? "Run failed")
|
|
288
|
-
: undefined);
|
|
289
|
-
if (errorText !== undefined) {
|
|
376
|
+
if (task.outputSchema !== undefined && capturedStructured === undefined) {
|
|
290
377
|
emit({
|
|
291
378
|
_tag: "RunSettled",
|
|
292
379
|
outcome: {
|
|
293
380
|
_tag: "Failed",
|
|
294
|
-
errorText:
|
|
381
|
+
errorText:
|
|
382
|
+
"Agent finished without calling structured_output; no structured result matching output_schema was produced.",
|
|
295
383
|
partialText,
|
|
296
384
|
},
|
|
297
385
|
});
|
|
298
386
|
return;
|
|
299
387
|
}
|
|
388
|
+
let structuredResult;
|
|
389
|
+
if (capturedStructured) {
|
|
390
|
+
try {
|
|
391
|
+
structuredResult = {
|
|
392
|
+
...capturedStructured,
|
|
393
|
+
artifactPath: persistStructuredResultArtifact(
|
|
394
|
+
getAgentDir(),
|
|
395
|
+
capturedStructured.json,
|
|
396
|
+
),
|
|
397
|
+
};
|
|
398
|
+
} catch (error) {
|
|
399
|
+
emit({
|
|
400
|
+
_tag: "RunSettled",
|
|
401
|
+
outcome: {
|
|
402
|
+
_tag: "Failed",
|
|
403
|
+
errorText: `Structured result artifact could not be persisted: ${boundedError(error)}`,
|
|
404
|
+
partialText,
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
300
410
|
emit({
|
|
301
411
|
_tag: "RunSettled",
|
|
302
|
-
outcome: {
|
|
412
|
+
outcome: {
|
|
413
|
+
_tag: "Completed",
|
|
414
|
+
finalText: prompt.finalText,
|
|
415
|
+
...(structuredResult ? { structuredResult } : {}),
|
|
416
|
+
},
|
|
303
417
|
});
|
|
304
418
|
};
|
|
305
419
|
|
|
420
|
+
/**
|
|
421
|
+
* A Pi lifecycle event is evidence, not the lease itself. The exact
|
|
422
|
+
* prompt() Promise must also settle before a terminal outcome is trusted;
|
|
423
|
+
* otherwise agent_settled can publish Completed and hide a later reject.
|
|
424
|
+
*/
|
|
425
|
+
const maybeSettle = (prompt: ActivePrompt) => {
|
|
426
|
+
if (
|
|
427
|
+
state.closed ||
|
|
428
|
+
state.activePrompt !== prompt ||
|
|
429
|
+
prompt.cancelled ||
|
|
430
|
+
state.settled ||
|
|
431
|
+
!prompt.promptSettled
|
|
432
|
+
) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (prompt.promptError !== undefined) {
|
|
436
|
+
settle(prompt);
|
|
437
|
+
} else if (!prompt.lifecycleStarted) {
|
|
438
|
+
prompt.promptError =
|
|
439
|
+
"Pi prompt completed without lifecycle events; no agent response was observed.";
|
|
440
|
+
settle(prompt);
|
|
441
|
+
} else if (!prompt.lifecycleSettled) {
|
|
442
|
+
prompt.promptError =
|
|
443
|
+
"Pi prompt completed without an agent_settled lifecycle event.";
|
|
444
|
+
settle(prompt);
|
|
445
|
+
} else {
|
|
446
|
+
settle(prompt);
|
|
447
|
+
}
|
|
448
|
+
if (state.activePrompt === prompt && prompt.promptSettled) {
|
|
449
|
+
state.activePrompt = undefined;
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
306
453
|
const handleEvent = (event: AgentSessionEvent) => {
|
|
307
454
|
if (state.closed) return;
|
|
455
|
+
const activePrompt = state.activePrompt;
|
|
456
|
+
if (!activePrompt) return;
|
|
457
|
+
if (activePrompt.cancelled) {
|
|
458
|
+
if (event.type === "agent_start") {
|
|
459
|
+
// abort() can return during Pi's preflight while prompt() remains
|
|
460
|
+
// pending. If that preflight later enters the agent lifecycle,
|
|
461
|
+
// abort again before it can reach a provider or tool. Never reopen
|
|
462
|
+
// the event gate: native events carry no generation identity.
|
|
463
|
+
try {
|
|
464
|
+
void session.abort().catch(() => undefined);
|
|
465
|
+
} catch {
|
|
466
|
+
// The original interrupt still owns bounded cleanup.
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
308
471
|
switch (event.type) {
|
|
309
472
|
case "agent_start":
|
|
310
473
|
// Extensions may register tools between runs; guard new ones too.
|
|
311
474
|
toolTimeout.apply(session);
|
|
312
|
-
|
|
475
|
+
activePrompt.lifecycleStarted = true;
|
|
313
476
|
emit({ _tag: "RunStarted" });
|
|
314
477
|
break;
|
|
315
478
|
case "message_update": {
|
|
316
479
|
const streamEvent = event.assistantMessageEvent;
|
|
317
480
|
if (streamEvent.type === "text_delta") {
|
|
481
|
+
activePrompt.liveText = (
|
|
482
|
+
activePrompt.liveText + streamEvent.delta
|
|
483
|
+
).slice(-PARTIAL_TEXT_MAX_LENGTH);
|
|
318
484
|
emit({
|
|
319
485
|
_tag: "AssistantDelta",
|
|
320
486
|
kind: "text",
|
|
@@ -335,9 +501,14 @@ const makePiSession = (
|
|
|
335
501
|
const text = userText(event.message as Message);
|
|
336
502
|
if (text.trim()) emit({ _tag: "UserMessage", text });
|
|
337
503
|
} else if (role === "assistant") {
|
|
504
|
+
const assistant = event.message as AssistantMessage;
|
|
505
|
+
activePrompt.lastAssistant = assistant;
|
|
506
|
+
const text = assistantText(assistant);
|
|
507
|
+
if (text) activePrompt.finalText = text;
|
|
508
|
+
activePrompt.liveText = "";
|
|
338
509
|
emit({
|
|
339
510
|
_tag: "AssistantMessage",
|
|
340
|
-
parts: assistantParts(
|
|
511
|
+
parts: assistantParts(assistant),
|
|
341
512
|
});
|
|
342
513
|
emitUsage();
|
|
343
514
|
emit({ _tag: "MetaChanged", meta: currentMeta() });
|
|
@@ -346,6 +517,12 @@ const makePiSession = (
|
|
|
346
517
|
break;
|
|
347
518
|
}
|
|
348
519
|
case "tool_execution_start":
|
|
520
|
+
toolRenderer.start(
|
|
521
|
+
event.toolCallId,
|
|
522
|
+
event.toolName,
|
|
523
|
+
event.args,
|
|
524
|
+
session.getToolDefinition(event.toolName),
|
|
525
|
+
);
|
|
349
526
|
emit({
|
|
350
527
|
_tag: "ToolStart",
|
|
351
528
|
toolId: event.toolCallId,
|
|
@@ -354,6 +531,12 @@ const makePiSession = (
|
|
|
354
531
|
});
|
|
355
532
|
break;
|
|
356
533
|
case "tool_execution_update":
|
|
534
|
+
toolRenderer.update(
|
|
535
|
+
event.toolCallId,
|
|
536
|
+
event.toolName,
|
|
537
|
+
event.args,
|
|
538
|
+
event.partialResult,
|
|
539
|
+
);
|
|
357
540
|
emit({
|
|
358
541
|
_tag: "ToolUpdate",
|
|
359
542
|
toolId: event.toolCallId,
|
|
@@ -361,6 +544,12 @@ const makePiSession = (
|
|
|
361
544
|
});
|
|
362
545
|
break;
|
|
363
546
|
case "tool_execution_end":
|
|
547
|
+
toolRenderer.end(
|
|
548
|
+
event.toolCallId,
|
|
549
|
+
event.toolName,
|
|
550
|
+
event.result,
|
|
551
|
+
event.isError,
|
|
552
|
+
);
|
|
364
553
|
emit({
|
|
365
554
|
_tag: "ToolEnd",
|
|
366
555
|
toolId: event.toolCallId,
|
|
@@ -385,7 +574,8 @@ const makePiSession = (
|
|
|
385
574
|
});
|
|
386
575
|
break;
|
|
387
576
|
case "agent_settled":
|
|
388
|
-
|
|
577
|
+
activePrompt.lifecycleSettled = true;
|
|
578
|
+
maybeSettle(activePrompt);
|
|
389
579
|
break;
|
|
390
580
|
}
|
|
391
581
|
};
|
|
@@ -400,17 +590,61 @@ const makePiSession = (
|
|
|
400
590
|
} catch {
|
|
401
591
|
// Continue with abort/dispose.
|
|
402
592
|
}
|
|
403
|
-
await shutdownAndDisposeChildSession(session, {
|
|
593
|
+
const shutdown = await shutdownAndDisposeChildSession(session, {
|
|
404
594
|
abort: true,
|
|
405
|
-
timeoutMs: CHILD_SHUTDOWN_TIMEOUT_MS,
|
|
595
|
+
timeoutMs: options.shutdownTimeoutMs ?? CHILD_SHUTDOWN_TIMEOUT_MS,
|
|
406
596
|
});
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
597
|
+
const promptQuiescent =
|
|
598
|
+
state.activePrompt === undefined || state.activePrompt.promptSettled;
|
|
599
|
+
const cleanupMessages = [...shutdown.errors];
|
|
600
|
+
let cleanupUncertain =
|
|
601
|
+
shutdown.errors.length > 0 || shutdown.timedOut || !promptQuiescent;
|
|
602
|
+
if (shutdown.timedOut)
|
|
603
|
+
cleanupMessages.push("cleanup deadline exceeded");
|
|
604
|
+
if (!promptQuiescent) {
|
|
605
|
+
cleanupMessages.push(
|
|
606
|
+
"active prompt did not quiesce before session shutdown",
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// A preflight hook can outlive abort() and continue touching the child
|
|
611
|
+
// cwd. Never reclaim a worktree until both the prompt and session
|
|
612
|
+
// cleanup have reached a known-safe terminal state.
|
|
613
|
+
if (
|
|
614
|
+
task.worktree &&
|
|
615
|
+
promptQuiescent &&
|
|
616
|
+
shutdown.ok &&
|
|
617
|
+
!shutdown.timedOut
|
|
618
|
+
) {
|
|
619
|
+
try {
|
|
620
|
+
const cleanup = await (options.worktreeCleanup ?? reclaimWorktree)(
|
|
621
|
+
task.worktree.repoCwd,
|
|
622
|
+
task.worktree,
|
|
623
|
+
{
|
|
624
|
+
timeoutMs: DIRECT_WORKTREE_CLEANUP_TIMEOUT_MS,
|
|
625
|
+
},
|
|
626
|
+
);
|
|
627
|
+
const warning = formatWorktreeCleanupWarning(
|
|
628
|
+
cleanup,
|
|
629
|
+
task.worktree.path,
|
|
630
|
+
);
|
|
631
|
+
if (warning) cleanupMessages.push(`worktree cleanup: ${warning}`);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
cleanupUncertain = true;
|
|
634
|
+
cleanupMessages.push(
|
|
635
|
+
`worktree cleanup failed: ${boundedError(error)}; checkout preserved at ${task.worktree.path}`,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
} else if (task.worktree) {
|
|
639
|
+
cleanupMessages.push(
|
|
640
|
+
`worktree cleanup skipped; checkout preserved at ${task.worktree.path}`,
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
if (cleanupMessages.length > 0) {
|
|
644
|
+
cleanupReceipt = {
|
|
645
|
+
uncertain: cleanupUncertain,
|
|
646
|
+
message: `${cleanupUncertain ? "Subagent cleanup is uncertain" : "Subagent cleanup"}: ${cleanupMessages.join("; ")}`,
|
|
647
|
+
};
|
|
414
648
|
}
|
|
415
649
|
Queue.endUnsafe(events);
|
|
416
650
|
}),
|
|
@@ -418,15 +652,64 @@ const makePiSession = (
|
|
|
418
652
|
|
|
419
653
|
/** Start a fresh run (v1 manager.run): fire-and-forget, errors -> events. */
|
|
420
654
|
const startRun = (text: string) => {
|
|
421
|
-
state.
|
|
655
|
+
if (state.activePrompt) {
|
|
656
|
+
throw new Error(
|
|
657
|
+
"Cannot start a Pi prompt while another prompt is active.",
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
const activePrompt: ActivePrompt = {
|
|
661
|
+
cancelled: false,
|
|
662
|
+
lifecycleStarted: false,
|
|
663
|
+
lifecycleSettled: false,
|
|
664
|
+
promptSettled: false,
|
|
665
|
+
finalText: "",
|
|
666
|
+
liveText: "",
|
|
667
|
+
promise: Promise.resolve(),
|
|
668
|
+
};
|
|
669
|
+
state.activePrompt = activePrompt;
|
|
670
|
+
capturedStructured = undefined;
|
|
422
671
|
state.settled = false;
|
|
423
672
|
emit({ _tag: "RunStarted" });
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
673
|
+
let prompt: Promise<void>;
|
|
674
|
+
try {
|
|
675
|
+
prompt = session.prompt(text, {
|
|
676
|
+
preflightResult: (accepted) => {
|
|
677
|
+
if (accepted && (activePrompt.cancelled || state.closed)) {
|
|
678
|
+
// This callback runs immediately before Pi enters
|
|
679
|
+
// _runAgentPrompt. Throwing here is the only synchronous seam
|
|
680
|
+
// that prevents a cancelled preflight from reviving after the
|
|
681
|
+
// adapter subscription and worktree ownership are gone.
|
|
682
|
+
throw new Error(
|
|
683
|
+
"Subagent prompt was cancelled during preflight.",
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
} catch (error) {
|
|
689
|
+
prompt = Promise.reject(error);
|
|
690
|
+
}
|
|
691
|
+
activePrompt.promise = prompt
|
|
692
|
+
.then(
|
|
693
|
+
() => undefined,
|
|
694
|
+
(error) => {
|
|
695
|
+
if (
|
|
696
|
+
!state.closed &&
|
|
697
|
+
state.activePrompt === activePrompt &&
|
|
698
|
+
!activePrompt.cancelled
|
|
699
|
+
) {
|
|
700
|
+
activePrompt.promptError = boundedError(error);
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
)
|
|
704
|
+
.finally(() => {
|
|
705
|
+
activePrompt.promptSettled = true;
|
|
706
|
+
maybeSettle(activePrompt);
|
|
707
|
+
if (state.activePrompt === activePrompt) {
|
|
708
|
+
if (activePrompt.cancelled || state.settled) {
|
|
709
|
+
state.activePrompt = undefined;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
});
|
|
430
713
|
};
|
|
431
714
|
|
|
432
715
|
// Session naming is best-effort.
|
|
@@ -444,6 +727,8 @@ const makePiSession = (
|
|
|
444
727
|
startRun(task.prompt);
|
|
445
728
|
|
|
446
729
|
return {
|
|
730
|
+
toolRenderer,
|
|
731
|
+
cleanupReceipt: () => cleanupReceipt,
|
|
447
732
|
meta: Effect.sync(currentMeta),
|
|
448
733
|
events: Stream.fromQueue(events),
|
|
449
734
|
send: (text) =>
|
|
@@ -451,7 +736,21 @@ const makePiSession = (
|
|
|
451
736
|
if (state.closed) {
|
|
452
737
|
return new SendError({ message: "Subagent session is closed." });
|
|
453
738
|
}
|
|
454
|
-
|
|
739
|
+
const pendingRestart = state.pendingRestart;
|
|
740
|
+
if (pendingRestart) {
|
|
741
|
+
return new SendError({
|
|
742
|
+
message: pendingRestart.cancelled
|
|
743
|
+
? "Subagent cancellation is still in progress."
|
|
744
|
+
: "A subagent restart is already pending.",
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
const activePrompt = state.activePrompt;
|
|
748
|
+
if (activePrompt?.cancelled) {
|
|
749
|
+
return new SendError({
|
|
750
|
+
message: "Subagent cancellation is still in progress.",
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
if (activePrompt && session.isStreaming) {
|
|
455
754
|
// Steer the active run via the SDK's queue; queue_update events
|
|
456
755
|
// render it, message_end(user) lands it in the transcript. A
|
|
457
756
|
// rejected steer is a real send failure, not a diagnostic.
|
|
@@ -460,34 +759,115 @@ const makePiSession = (
|
|
|
460
759
|
catch: (error) => new SendError({ message: boundedError(error) }),
|
|
461
760
|
}).pipe(Effect.asVoid);
|
|
462
761
|
}
|
|
762
|
+
if (
|
|
763
|
+
activePrompt &&
|
|
764
|
+
!activePrompt.lifecycleSettled &&
|
|
765
|
+
!activePrompt.promptSettled
|
|
766
|
+
) {
|
|
767
|
+
return new SendError({
|
|
768
|
+
message:
|
|
769
|
+
"Subagent prompt is still starting; wait for it to run or settle before sending.",
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
if (activePrompt) {
|
|
773
|
+
// Pi emits agent_settled before prompt() resolves. Preserve the
|
|
774
|
+
// same-session restart contract while closing that microtask gap
|
|
775
|
+
// without ever overlapping two native prompts.
|
|
776
|
+
const restart: PendingRestart = {
|
|
777
|
+
cancelled: false,
|
|
778
|
+
terminalEmitted: false,
|
|
779
|
+
};
|
|
780
|
+
state.pendingRestart = restart;
|
|
781
|
+
return Effect.tryPromise({
|
|
782
|
+
try: async (signal) => {
|
|
783
|
+
const cancelPendingRestart = () => {
|
|
784
|
+
restart.cancelled = true;
|
|
785
|
+
if (state.pendingRestart === restart) {
|
|
786
|
+
state.pendingRestart = undefined;
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
signal.addEventListener("abort", cancelPendingRestart, {
|
|
790
|
+
once: true,
|
|
791
|
+
});
|
|
792
|
+
if (signal.aborted) cancelPendingRestart();
|
|
793
|
+
try {
|
|
794
|
+
await activePrompt.promise;
|
|
795
|
+
if (state.closed) {
|
|
796
|
+
throw new Error("Subagent session is closed.");
|
|
797
|
+
}
|
|
798
|
+
if (restart.cancelled) {
|
|
799
|
+
// interrupt emits the terminal event for this accepted
|
|
800
|
+
// restart. Resolve the send without starting a prompt so
|
|
801
|
+
// the manager keeps its restarting marker until that
|
|
802
|
+
// terminal event is folded.
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
state.pendingRestart = undefined;
|
|
806
|
+
startRun(text);
|
|
807
|
+
} finally {
|
|
808
|
+
signal.removeEventListener("abort", cancelPendingRestart);
|
|
809
|
+
if (state.pendingRestart === restart) {
|
|
810
|
+
state.pendingRestart = undefined;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
catch: (error) => new SendError({ message: boundedError(error) }),
|
|
815
|
+
}).pipe(Effect.asVoid);
|
|
816
|
+
}
|
|
463
817
|
return Effect.sync(() => startRun(text));
|
|
464
818
|
}),
|
|
465
819
|
interrupt: Effect.promise(async () => {
|
|
466
820
|
if (state.closed) return;
|
|
821
|
+
const pendingRestart = state.pendingRestart;
|
|
822
|
+
if (pendingRestart) pendingRestart.cancelled = true;
|
|
823
|
+
const activePrompt = state.activePrompt;
|
|
824
|
+
if (activePrompt) {
|
|
825
|
+
activePrompt.cancelled = true;
|
|
826
|
+
activePrompt.partialTextAtCancel = pendingRestart
|
|
827
|
+
? undefined
|
|
828
|
+
: activePrompt.liveText || activePrompt.finalText || undefined;
|
|
829
|
+
}
|
|
467
830
|
try {
|
|
468
831
|
session.clearQueue();
|
|
469
832
|
} catch {
|
|
470
833
|
// Abort regardless.
|
|
471
834
|
}
|
|
472
835
|
await session.abort().catch(() => undefined);
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
// manager
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
836
|
+
// isStreaming is false during Pi preflight, so it cannot prove the
|
|
837
|
+
// invocation is quiescent. Wait for the exact prompt Promise instead;
|
|
838
|
+
// the manager bounds this effect at 5s and force-disposes on timeout.
|
|
839
|
+
await activePrompt?.promise;
|
|
840
|
+
if (
|
|
841
|
+
!state.closed &&
|
|
842
|
+
pendingRestart &&
|
|
843
|
+
!pendingRestart.terminalEmitted
|
|
844
|
+
) {
|
|
845
|
+
pendingRestart.terminalEmitted = true;
|
|
846
|
+
if (state.pendingRestart === pendingRestart) {
|
|
847
|
+
state.pendingRestart = undefined;
|
|
848
|
+
}
|
|
849
|
+
state.settled = true;
|
|
850
|
+
emit({ _tag: "RunSettled", outcome: { _tag: "Interrupted" } });
|
|
851
|
+
return;
|
|
479
852
|
}
|
|
480
|
-
// No streaming run means no agent_settled will arrive; emit the
|
|
481
|
-
// terminal event (once) so the run cannot look running forever.
|
|
482
853
|
if (!state.closed && !state.settled) {
|
|
483
854
|
state.settled = true;
|
|
484
|
-
emit({
|
|
855
|
+
emit({
|
|
856
|
+
_tag: "RunSettled",
|
|
857
|
+
outcome: {
|
|
858
|
+
_tag: "Interrupted",
|
|
859
|
+
partialText: activePrompt?.partialTextAtCancel,
|
|
860
|
+
},
|
|
861
|
+
});
|
|
485
862
|
}
|
|
486
863
|
}),
|
|
487
864
|
} satisfies SubagentSession;
|
|
488
865
|
});
|
|
489
866
|
|
|
490
|
-
export const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
867
|
+
export const makePiBackend = (options: PiBackendOptions = {}) =>
|
|
868
|
+
({
|
|
869
|
+
name: "pi",
|
|
870
|
+
spawn: (task) => makePiSession(task, options),
|
|
871
|
+
}) satisfies SubagentBackend;
|
|
872
|
+
|
|
873
|
+
export const piBackend = makePiBackend();
|