@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
|
@@ -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 {
|
|
@@ -68,6 +69,8 @@ export interface SpawnTask {
|
|
|
68
69
|
readonly tools?: readonly string[];
|
|
69
70
|
/** Agent type that supplied the above, for the session label. */
|
|
70
71
|
readonly agentTypeName?: string;
|
|
72
|
+
/** Optional JSON Schema for one terminating, validated child result. */
|
|
73
|
+
readonly outputSchema?: unknown;
|
|
71
74
|
/**
|
|
72
75
|
* Isolated git worktree this child runs in, created by the tool layer. The
|
|
73
76
|
* backend only reclaims it when the session scope closes; it does not know
|
|
@@ -141,7 +144,11 @@ export interface QueuedMessage {
|
|
|
141
144
|
// --- Events ------------------------------------------------------------------
|
|
142
145
|
|
|
143
146
|
export type RunOutcome =
|
|
144
|
-
| {
|
|
147
|
+
| {
|
|
148
|
+
readonly _tag: "Completed";
|
|
149
|
+
readonly finalText: string;
|
|
150
|
+
readonly structuredResult?: StructuredSubagentResult;
|
|
151
|
+
}
|
|
145
152
|
| {
|
|
146
153
|
readonly _tag: "Failed";
|
|
147
154
|
readonly errorText: string;
|
|
@@ -215,22 +222,35 @@ export interface SubagentSnapshot {
|
|
|
215
222
|
readonly prompt: string;
|
|
216
223
|
readonly cwd: string;
|
|
217
224
|
readonly status: SubagentStatus;
|
|
225
|
+
readonly outcome?: SubagentOutcome;
|
|
226
|
+
readonly worktreeBranch?: string;
|
|
218
227
|
readonly createdAt: number;
|
|
219
228
|
readonly settledAt?: number;
|
|
220
229
|
readonly errorText?: string;
|
|
221
230
|
readonly meta: SubagentMeta;
|
|
222
231
|
readonly usage: { readonly tokens?: number; readonly contextWindow?: number };
|
|
223
232
|
readonly transcript: ReadonlyArray<TranscriptItem>;
|
|
233
|
+
/** Monotonic version bumped on every transcript mutation (see manager). */
|
|
234
|
+
readonly transcriptVersion: number;
|
|
224
235
|
/** Streaming assistant buffers, cleared when the finalized message lands. */
|
|
225
236
|
readonly liveAssistant?: { readonly text: string; readonly thinking: string };
|
|
226
237
|
readonly liveTools: ReadonlyArray<LiveToolState>;
|
|
227
238
|
readonly queued: ReadonlyArray<QueuedMessage>;
|
|
228
239
|
/** Final text of the most recent completed run (v1 `finalOutput`). */
|
|
229
240
|
readonly finalText: string;
|
|
241
|
+
/** Present only when this run supplied and satisfied output_schema. */
|
|
242
|
+
readonly structuredResult?: StructuredSubagentResult;
|
|
230
243
|
/** Count of finalized assistant messages (for subagent_check). */
|
|
231
244
|
readonly turns: number;
|
|
232
245
|
}
|
|
233
246
|
|
|
247
|
+
export interface StructuredSubagentResult {
|
|
248
|
+
readonly value: unknown;
|
|
249
|
+
readonly json: string;
|
|
250
|
+
readonly byteLength: number;
|
|
251
|
+
readonly artifactPath: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
234
254
|
/** Final text, or the live streaming buffer while a run is active (v1 `latestOutput`). */
|
|
235
255
|
export function latestText(snap: SubagentSnapshot) {
|
|
236
256
|
const live = snap.liveAssistant?.text.trim();
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
Stream,
|
|
28
28
|
} from "effect";
|
|
29
29
|
import type { SubagentBackend, SubagentSession } from "./backend.ts";
|
|
30
|
+
import type { AgentToolRenderer } from "../../shared/agent-tool-renderer.ts";
|
|
30
31
|
import { BackendRegistry } from "./backend.ts";
|
|
31
32
|
import type {
|
|
32
33
|
BackendName,
|
|
@@ -63,7 +64,7 @@ const ENTRY_CLOSE_TIMEOUT_MS = 10_000;
|
|
|
63
64
|
* First-response watchdog: a run whose provider accepts the request but
|
|
64
65
|
* never emits an assistant event is settled as a failure so it cannot
|
|
65
66
|
* occupy a concurrency slot forever. Matches the workflow runner's
|
|
66
|
-
*
|
|
67
|
+
* MODEL_PROGRESS_TIMEOUT_MS (extensions/workflows/runner.ts).
|
|
67
68
|
*/
|
|
68
69
|
export const FIRST_RESPONSE_TIMEOUT_MS = 45_000;
|
|
69
70
|
const ERROR_TEXT_MAX_LENGTH = 4_096;
|
|
@@ -92,6 +93,9 @@ function appendTranscript(snapshot: MutableSnapshot, item: TranscriptItem) {
|
|
|
92
93
|
snapshot.transcript.length - MAX_TRANSCRIPT_ITEMS,
|
|
93
94
|
);
|
|
94
95
|
}
|
|
96
|
+
// Monotonic version so UI projections can detect transcript changes by
|
|
97
|
+
// O(1) comparison instead of scanning the mutable array.
|
|
98
|
+
snapshot.transcriptVersion++;
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
// --- Internal state -----------------------------------------------------------
|
|
@@ -105,16 +109,21 @@ interface MutableSnapshot {
|
|
|
105
109
|
prompt: string;
|
|
106
110
|
cwd: string;
|
|
107
111
|
status: SubagentStatus;
|
|
112
|
+
outcome?: SubagentSnapshot["outcome"];
|
|
113
|
+
worktreeBranch?: string;
|
|
108
114
|
createdAt: number;
|
|
109
115
|
settledAt?: number;
|
|
110
116
|
errorText?: string;
|
|
111
117
|
meta: SubagentMeta;
|
|
112
118
|
usage: { tokens?: number; contextWindow?: number };
|
|
113
119
|
transcript: TranscriptItem[];
|
|
120
|
+
/** Bumped on every transcript mutation; UI caches key on this. */
|
|
121
|
+
transcriptVersion: number;
|
|
114
122
|
liveAssistant?: { text: string; thinking: string };
|
|
115
123
|
liveTools: LiveToolState[];
|
|
116
124
|
queued: SubagentSnapshot["queued"];
|
|
117
125
|
finalText: string;
|
|
126
|
+
structuredResult?: SubagentSnapshot["structuredResult"];
|
|
118
127
|
turns: number;
|
|
119
128
|
}
|
|
120
129
|
|
|
@@ -137,6 +146,8 @@ interface Entry {
|
|
|
137
146
|
export interface SubagentReadModel {
|
|
138
147
|
list(): ReadonlyArray<SubagentSnapshot>;
|
|
139
148
|
get(id: string): SubagentSnapshot | undefined;
|
|
149
|
+
/** Native tool projection retained by the live child session, when present. */
|
|
150
|
+
getToolRenderer?(id: string): AgentToolRenderer | undefined;
|
|
140
151
|
size(): number;
|
|
141
152
|
/** Any-change notification (footer status, dashboard). */
|
|
142
153
|
subscribe(listener: () => void): () => void;
|
|
@@ -299,7 +310,20 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
299
310
|
};
|
|
300
311
|
|
|
301
312
|
const closeEntryScope = (entry: Entry) =>
|
|
302
|
-
Scope.close(entry.scope, Exit.void).pipe(
|
|
313
|
+
Scope.close(entry.scope, Exit.void).pipe(
|
|
314
|
+
Effect.tap(() =>
|
|
315
|
+
Effect.sync(() => {
|
|
316
|
+
const receipt = entry.session.cleanupReceipt?.();
|
|
317
|
+
if (!receipt?.uncertain) return;
|
|
318
|
+
const current = entry.snapshot.errorText;
|
|
319
|
+
entry.snapshot.errorText = current
|
|
320
|
+
? `${current}; ${receipt.message}`
|
|
321
|
+
: receipt.message;
|
|
322
|
+
notify(entry.snapshot.id);
|
|
323
|
+
}),
|
|
324
|
+
),
|
|
325
|
+
Effect.ignore,
|
|
326
|
+
);
|
|
303
327
|
|
|
304
328
|
const pruneSettled = () => {
|
|
305
329
|
if (entries.size <= MAX_TRACKED) return;
|
|
@@ -337,25 +361,31 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
337
361
|
switch (outcome._tag) {
|
|
338
362
|
case "Completed":
|
|
339
363
|
s.status = "done";
|
|
364
|
+
s.outcome = "completed";
|
|
340
365
|
s.errorText = undefined;
|
|
341
366
|
s.finalText = outcome.finalText.slice(0, FINAL_TEXT_MAX_LENGTH);
|
|
367
|
+
s.structuredResult = outcome.structuredResult;
|
|
342
368
|
break;
|
|
343
369
|
case "Failed":
|
|
344
370
|
s.status = "error";
|
|
371
|
+
s.outcome = "failed";
|
|
345
372
|
s.errorText = bounded(outcome.errorText);
|
|
346
373
|
// Never let a failed run report the previous run's successful output.
|
|
347
374
|
s.finalText = (outcome.partialText ?? "").slice(
|
|
348
375
|
0,
|
|
349
376
|
FINAL_TEXT_MAX_LENGTH,
|
|
350
377
|
);
|
|
378
|
+
s.structuredResult = undefined;
|
|
351
379
|
break;
|
|
352
380
|
case "Interrupted":
|
|
353
381
|
s.status = "error";
|
|
382
|
+
s.outcome = "interrupted";
|
|
354
383
|
s.errorText = "Run was aborted";
|
|
355
384
|
s.finalText = (outcome.partialText ?? "").slice(
|
|
356
385
|
0,
|
|
357
386
|
FINAL_TEXT_MAX_LENGTH,
|
|
358
387
|
);
|
|
388
|
+
s.structuredResult = undefined;
|
|
359
389
|
break;
|
|
360
390
|
}
|
|
361
391
|
s.liveAssistant = undefined;
|
|
@@ -417,8 +447,10 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
417
447
|
case "RunStarted":
|
|
418
448
|
entry.restarting = false;
|
|
419
449
|
s.status = "running";
|
|
450
|
+
s.outcome = undefined;
|
|
420
451
|
s.settledAt = undefined;
|
|
421
452
|
s.errorText = undefined;
|
|
453
|
+
s.structuredResult = undefined;
|
|
422
454
|
armWatchdog(entry);
|
|
423
455
|
break;
|
|
424
456
|
case "RunSettled":
|
|
@@ -578,10 +610,14 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
578
610
|
prompt: task.prompt,
|
|
579
611
|
cwd: task.cwd,
|
|
580
612
|
status: "running",
|
|
613
|
+
...(task.worktree
|
|
614
|
+
? { worktreeBranch: task.worktree.branch }
|
|
615
|
+
: {}),
|
|
581
616
|
createdAt: Date.now(),
|
|
582
617
|
meta,
|
|
583
618
|
usage: { contextWindow: meta.contextWindow },
|
|
584
619
|
transcript: [],
|
|
620
|
+
transcriptVersion: 0,
|
|
585
621
|
liveTools: [],
|
|
586
622
|
queued: [],
|
|
587
623
|
finalText: "",
|
|
@@ -792,6 +828,7 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
792
828
|
const view: SubagentReadModel = {
|
|
793
829
|
list: () => [...entries.values()].map((entry) => entry.snapshot),
|
|
794
830
|
get: (id) => entries.get(id)?.snapshot,
|
|
831
|
+
getToolRenderer: (id) => entries.get(id)?.session.toolRenderer,
|
|
795
832
|
size: () => entries.size,
|
|
796
833
|
subscribe: (listener) => {
|
|
797
834
|
listeners.add(listener);
|
|
@@ -17,8 +17,8 @@ export const SUBAGENT_SCHEMA_BUDGETS = Object.freeze({
|
|
|
17
17
|
|
|
18
18
|
/** Describes subagent_spawn, including the fixed concurrency cap. */
|
|
19
19
|
export const SUBAGENT_SPAWN_TOOL_DESCRIPTION =
|
|
20
|
-
"Spawn a background
|
|
21
|
-
`Max ${MAX_RUNNING} subagents can
|
|
20
|
+
"Spawn a background Pi subagent with isolated context and child-safe tools. Returns immediately; its result arrives automatically. It cannot see this chat, ask the user, or orchestrate. Use trusted directories. " +
|
|
21
|
+
`Max ${MAX_RUNNING} subagents can run at once.`;
|
|
22
22
|
|
|
23
23
|
/** UTF-8 bounded, whitespace-normalized text for the parent-facing roster. */
|
|
24
24
|
function boundedPurpose(description: string) {
|
|
@@ -152,6 +152,7 @@ export const SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS = {
|
|
|
152
152
|
'Optional "provider/model-id" or current-provider model override. Omit to use the preset, configured role, or parent default. Never guess a model name.',
|
|
153
153
|
reasoningEffort:
|
|
154
154
|
"Optional child thinking level. Honor the user's requested level. Otherwise choose a level supported by the resolved child model based on the selected role and task difficulty. An explicit value overrides a role default.",
|
|
155
|
+
outputSchema: "Optional result JSON Schema.",
|
|
155
156
|
};
|
|
156
157
|
|
|
157
158
|
/** The exact name/description/wire-schema source used by registration/tests. */
|
|
@@ -193,6 +194,15 @@ export function createSubagentSpawnToolSurface(
|
|
|
193
194
|
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.reasoningEffort,
|
|
194
195
|
}),
|
|
195
196
|
),
|
|
197
|
+
output_schema: Type.Optional(
|
|
198
|
+
Type.Object(
|
|
199
|
+
{},
|
|
200
|
+
{
|
|
201
|
+
additionalProperties: true,
|
|
202
|
+
description: SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS.outputSchema,
|
|
203
|
+
},
|
|
204
|
+
),
|
|
205
|
+
),
|
|
196
206
|
}),
|
|
197
207
|
};
|
|
198
208
|
}
|
|
@@ -207,6 +217,7 @@ export function buildSubagentSpawnResult(options: {
|
|
|
207
217
|
agentTypeName?: string;
|
|
208
218
|
tools?: readonly string[];
|
|
209
219
|
worktreeBranch?: string;
|
|
220
|
+
structured?: boolean;
|
|
210
221
|
}) {
|
|
211
222
|
const typeNote = options.agentTypeName
|
|
212
223
|
? ` Agent type "${options.agentTypeName}" applied.`
|
|
@@ -226,8 +237,11 @@ export function buildSubagentSpawnResult(options: {
|
|
|
226
237
|
const worktreeNote = options.worktreeBranch
|
|
227
238
|
? ` Isolated in its own worktree on branch "${options.worktreeBranch}" — its edits are invisible here until you merge that branch. The checkout stays available for later send/review and is reclaimed on Session retirement only when bounded inspection proves it empty.`
|
|
228
239
|
: "";
|
|
240
|
+
const structuredNote = options.structured
|
|
241
|
+
? " This run must finish with the requested validated structured result."
|
|
242
|
+
: "";
|
|
229
243
|
return (
|
|
230
|
-
`Spawned subagent ${options.id} "${options.title}" (${options.harness}: ${options.modelLabel}, ${options.cwd}).${typeNote}${toolNote}${worktreeNote}\n` +
|
|
244
|
+
`Spawned subagent ${options.id} "${options.title}" (${options.harness}: ${options.modelLabel}, ${options.cwd}).${typeNote}${toolNote}${worktreeNote}${structuredNote}\n` +
|
|
231
245
|
`It runs in the background — keep working on independent work. If none remains in an interactive session, briefly tell the user it is still running and end your turn; its result is delivered automatically and you are automatically re-invoked when it finishes. Do not poll or call subagent_wait merely because a later step depends on it. ` +
|
|
232
246
|
`Use subagent_wait(ids: ["${options.id}"]) only if the user explicitly asked you to keep the current response open for this result, or a non-interactive automation must return it in the same invocation; subagent_cancel stops it, subagent_check peeks at a running one, subagent_list shows all.`
|
|
233
247
|
);
|
|
@@ -285,8 +299,11 @@ export const SUBAGENT_CHECK_PARAMETER_DESCRIPTIONS = {
|
|
|
285
299
|
export const SUBAGENT_LIST_TOOL_DESCRIPTION =
|
|
286
300
|
"List all subagents (running and finished) with their status.";
|
|
287
301
|
|
|
288
|
-
|
|
289
|
-
|
|
302
|
+
const SUBAGENT_RESULT_TRANSPORT_INSTRUCTION =
|
|
303
|
+
"(This result is already shown to the user. Act on it and relay only the decisions or next steps — do not repeat it verbatim.)";
|
|
304
|
+
|
|
305
|
+
/** Builds the user-visible child completion/failure projection. */
|
|
306
|
+
export function buildSubagentResultDisplayMessage(options: {
|
|
290
307
|
id: string;
|
|
291
308
|
title: string;
|
|
292
309
|
status: "running" | "done" | "error";
|
|
@@ -297,9 +314,34 @@ export function buildSubagentResultMessage(options: {
|
|
|
297
314
|
let text = `Subagent ${options.id} "${options.title}" ${verb}.`;
|
|
298
315
|
if (options.errorText) text += `\nError: ${options.errorText}`;
|
|
299
316
|
text += `\n\n${options.output}`;
|
|
317
|
+
return text;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Builds the child completion/failure wrapper injected into the parent model's context. */
|
|
321
|
+
export function buildSubagentResultMessage(options: {
|
|
322
|
+
id: string;
|
|
323
|
+
title: string;
|
|
324
|
+
status: "running" | "done" | "error";
|
|
325
|
+
errorText?: string;
|
|
326
|
+
output: string;
|
|
327
|
+
}) {
|
|
328
|
+
let text = buildSubagentResultDisplayMessage(options);
|
|
300
329
|
// This message is already displayed to the user, so tell the parent to act on
|
|
301
330
|
// it rather than reprint it verbatim.
|
|
302
|
-
text +=
|
|
303
|
-
"\n\n(This result is already shown to the user. Act on it and relay only the decisions or next steps — do not repeat it verbatim.)";
|
|
331
|
+
text += `\n\n${SUBAGENT_RESULT_TRANSPORT_INSTRUCTION}`;
|
|
304
332
|
return text;
|
|
305
333
|
}
|
|
334
|
+
|
|
335
|
+
/** Remove the transport-only suffix from results persisted before the split. */
|
|
336
|
+
export function stripSubagentResultTransportInstruction(content: string) {
|
|
337
|
+
const separator = `\n\n${SUBAGENT_RESULT_TRANSPORT_INSTRUCTION}`;
|
|
338
|
+
const withoutBatchedSeparators = content.replaceAll(
|
|
339
|
+
`${separator}\n\nSubagent `,
|
|
340
|
+
"\n\nSubagent ",
|
|
341
|
+
);
|
|
342
|
+
return (
|
|
343
|
+
withoutBatchedSeparators.endsWith(separator)
|
|
344
|
+
? withoutBatchedSeparators.slice(0, -separator.length)
|
|
345
|
+
: withoutBatchedSeparators
|
|
346
|
+
).trimEnd();
|
|
347
|
+
}
|
|
@@ -20,6 +20,7 @@ export interface ResultProjection {
|
|
|
20
20
|
readonly text: string;
|
|
21
21
|
readonly truncated: boolean;
|
|
22
22
|
readonly artifactPath?: string;
|
|
23
|
+
readonly artifactSaveFailed?: boolean;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
function sliceStartToUtf8Bytes(content: string, maxBytes: number) {
|
|
@@ -76,6 +77,38 @@ export function persistResultArtifact(agentDir: string, content: string) {
|
|
|
76
77
|
return artifactPath;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
/** Persist one complete validated structured value under a JSON identity. */
|
|
81
|
+
export function persistStructuredResultArtifact(
|
|
82
|
+
agentDir: string,
|
|
83
|
+
content: string,
|
|
84
|
+
) {
|
|
85
|
+
let directory = path.resolve(agentDir);
|
|
86
|
+
for (const segment of RESULT_ARTIFACT_DIR) {
|
|
87
|
+
directory = ensureDirectory(directory, segment);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const digest = createHash("sha256").update(content).digest("hex");
|
|
91
|
+
const artifactPath = path.join(directory, `${digest}.json`);
|
|
92
|
+
try {
|
|
93
|
+
writeFileSync(artifactPath, content, {
|
|
94
|
+
encoding: "utf8",
|
|
95
|
+
flag: "wx",
|
|
96
|
+
mode: 0o600,
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
100
|
+
const stat = lstatSync(artifactPath);
|
|
101
|
+
if (
|
|
102
|
+
!stat.isFile() ||
|
|
103
|
+
stat.isSymbolicLink() ||
|
|
104
|
+
readFileSync(artifactPath, "utf8") !== content
|
|
105
|
+
) {
|
|
106
|
+
throw new Error(`Structured result artifact collision: ${artifactPath}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return artifactPath;
|
|
110
|
+
}
|
|
111
|
+
|
|
79
112
|
/**
|
|
80
113
|
* Build the single model-visible projection used by automatic delivery and
|
|
81
114
|
* explicit waits. Short answers pass through byte-for-byte. Long answers keep
|
|
@@ -96,11 +129,13 @@ export function projectResult(
|
|
|
96
129
|
const tailLines = Math.max(1, options.maxLines - headLines);
|
|
97
130
|
|
|
98
131
|
let artifactPath: string | undefined;
|
|
132
|
+
let artifactSaveFailed = false;
|
|
99
133
|
try {
|
|
100
134
|
artifactPath = options.writeArtifact(content);
|
|
101
135
|
} catch {
|
|
102
136
|
// Delivery is more important than the optional recovery cache. The footer
|
|
103
137
|
// below stays explicit so a failed write never advertises a false path.
|
|
138
|
+
artifactSaveFailed = true;
|
|
104
139
|
}
|
|
105
140
|
|
|
106
141
|
let bodyBudget = options.maxBytes;
|
|
@@ -138,5 +173,6 @@ export function projectResult(
|
|
|
138
173
|
text,
|
|
139
174
|
truncated: true,
|
|
140
175
|
...(artifactPath ? { artifactPath } : {}),
|
|
176
|
+
...(artifactSaveFailed ? { artifactSaveFailed: true } : {}),
|
|
141
177
|
};
|
|
142
178
|
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import type { ConsumableResultDeliveryQueue } from "../../shared/result-delivery.ts";
|
|
2
|
+
import {
|
|
3
|
+
type CompletionOwner,
|
|
4
|
+
createCompletionInbox,
|
|
5
|
+
} from "../../shared/completion-inbox.ts";
|
|
6
|
+
|
|
1
7
|
export interface SubagentResultDeliveryOptions<T> {
|
|
2
8
|
/** True only when the parent has no run or queued continuation in flight. */
|
|
3
9
|
readonly isIdle: () => boolean;
|
|
4
10
|
/** Deliver one drained batch and wake the parent. */
|
|
5
11
|
readonly deliver: (results: readonly T[]) => void;
|
|
12
|
+
/** Current Pi Session transcript owner. */
|
|
13
|
+
readonly owner?: () => CompletionOwner | undefined;
|
|
6
14
|
}
|
|
7
15
|
|
|
8
16
|
/**
|
|
@@ -20,46 +28,63 @@ export interface SubagentResultDeliveryOptions<T> {
|
|
|
20
28
|
* The parent boundary wakes even if an earlier extension handler has already
|
|
21
29
|
* started another turn: Pi queues the follow-up into that active run.
|
|
22
30
|
*
|
|
23
|
-
* The
|
|
24
|
-
* is delivered, and whichever path
|
|
31
|
+
* The shared inbox is the one-shot gate: `subagent_wait` may consume a result
|
|
32
|
+
* before it is delivered, and whichever path claims first prevents duplicate
|
|
33
|
+
* delivery.
|
|
25
34
|
*/
|
|
26
35
|
export function createSubagentResultDelivery<T extends { id: string }>(
|
|
27
36
|
options: SubagentResultDeliveryOptions<T>,
|
|
28
37
|
) {
|
|
29
|
-
const
|
|
38
|
+
const inbox = createCompletionInbox<T>();
|
|
39
|
+
const owner = options.owner ?? (() => ({ sessionId: "test", epoch: 0 }));
|
|
30
40
|
|
|
31
41
|
const flush = () => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
const envelopes = inbox.claim(owner());
|
|
43
|
+
if (envelopes.length === 0) return;
|
|
44
|
+
const results = envelopes.map((envelope) => envelope.payload);
|
|
35
45
|
try {
|
|
36
46
|
options.deliver(results);
|
|
47
|
+
inbox.acknowledge(envelopes.map((envelope) => envelope.deliveryId));
|
|
37
48
|
} catch (error) {
|
|
38
49
|
// A synchronous session teardown may reject append/send. Preserve the
|
|
39
50
|
// original batch ahead of anything deferred re-entrantly while delivery
|
|
40
51
|
// ran, so a later boundary can retry without loss or reordering.
|
|
41
|
-
|
|
42
|
-
pending.clear();
|
|
43
|
-
for (const result of results) pending.set(result.id, result);
|
|
44
|
-
for (const result of current) pending.set(result.id, result);
|
|
52
|
+
inbox.retry(envelopes, owner());
|
|
45
53
|
throw error;
|
|
46
54
|
}
|
|
47
55
|
};
|
|
48
56
|
|
|
49
|
-
|
|
57
|
+
const queue = {
|
|
50
58
|
defer(result: T) {
|
|
51
|
-
|
|
59
|
+
const currentOwner = owner();
|
|
60
|
+
inbox.defer(
|
|
61
|
+
{
|
|
62
|
+
deliveryId: `subagent:${result.id}`,
|
|
63
|
+
owner: currentOwner ?? { sessionId: "unowned", epoch: 0 },
|
|
64
|
+
producer: "subagent",
|
|
65
|
+
producerId: result.id,
|
|
66
|
+
terminalRef: { kind: "subagent-snapshot", id: result.id },
|
|
67
|
+
wake: "follow-up",
|
|
68
|
+
payload: result,
|
|
69
|
+
},
|
|
70
|
+
currentOwner,
|
|
71
|
+
);
|
|
52
72
|
if (options.isIdle()) flush();
|
|
53
73
|
},
|
|
54
74
|
consume(ids: Iterable<string>) {
|
|
55
|
-
|
|
75
|
+
inbox.consume("subagent", ids);
|
|
56
76
|
},
|
|
57
77
|
/** Flush at the authoritative parent boundary. */
|
|
58
78
|
parentSettled() {
|
|
59
79
|
flush();
|
|
60
80
|
},
|
|
61
81
|
clear() {
|
|
62
|
-
|
|
82
|
+
inbox.clear();
|
|
83
|
+
},
|
|
84
|
+
size() {
|
|
85
|
+
return inbox.size();
|
|
63
86
|
},
|
|
87
|
+
inspectDeadLetters: inbox.inspectDeadLetters,
|
|
64
88
|
};
|
|
89
|
+
return queue satisfies ConsumableResultDeliveryQueue<T>;
|
|
65
90
|
}
|
|
@@ -11,8 +11,22 @@ import { BackendRegistry, type SubagentBackend } from "./backend.ts";
|
|
|
11
11
|
import { piBackend } from "./backends/pi.ts";
|
|
12
12
|
import type { BackendName } from "./domain.ts";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Test-only injection seam for extension-level tests that drive real spawns
|
|
16
|
+
* without a child pi session: production never sets it. The underscore-prefixed
|
|
17
|
+
* setter name makes any accidental production use self-evidently wrong.
|
|
18
|
+
*/
|
|
19
|
+
let testBackends: readonly SubagentBackend[] | undefined;
|
|
20
|
+
|
|
21
|
+
/** Test-only: replace the backends the manager can spawn against. */
|
|
22
|
+
export function __setSubagentTestBackends(
|
|
23
|
+
backends: readonly SubagentBackend[] | undefined,
|
|
24
|
+
) {
|
|
25
|
+
testBackends = backends;
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
const BackendRegistryLive = Layer.sync(BackendRegistry, () => {
|
|
15
|
-
const backends: SubagentBackend[] = [piBackend];
|
|
29
|
+
const backends: readonly SubagentBackend[] = testBackends ?? [piBackend];
|
|
16
30
|
return new Map<BackendName, SubagentBackend>(
|
|
17
31
|
backends.map((backend) => [backend.name, backend]),
|
|
18
32
|
);
|