@tt-a1i/openpi 0.5.0 → 0.6.1
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 +30 -20
- package/SETUP.md +10 -4
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/bin/openpi.js +25 -15
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +65 -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 +105 -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 +1271 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1181 -0
- package/extensions/ai-providers/cursor/provider.ts +1431 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/tool-bridge.ts +68 -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 +8 -1
- package/extensions/background-terminals/src/manager.ts +3 -5
- package/extensions/background-terminals/src/result-delivery.ts +43 -23
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +5 -1
- package/extensions/model-info/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +45 -1
- package/extensions/plan-mode/index.ts +75 -4
- package/extensions/setup/index.ts +15 -3
- package/extensions/shared/child-session.ts +39 -5
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/setup-config.ts +10 -1
- package/extensions/shared/structured-output.ts +154 -0
- package/extensions/subagents/index.ts +64 -7
- package/extensions/subagents/src/agent-types.ts +5 -17
- package/extensions/subagents/src/backends/pi.ts +130 -48
- package/extensions/subagents/src/backends/tool-preview.ts +29 -0
- package/extensions/subagents/src/domain.ts +16 -1
- package/extensions/subagents/src/manager.ts +7 -71
- package/extensions/subagents/src/prompt.ts +19 -5
- package/extensions/subagents/src/result-artifact.ts +32 -0
- package/extensions/subagents/src/result-delivery.ts +33 -14
- package/extensions/subagents/src/runtime.ts +10 -3
- package/extensions/ui-customization/footer.ts +16 -5
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +25 -2
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/completion-projection.ts +3 -1
- package/extensions/workflows/dashboard.ts +147 -21
- package/extensions/workflows/index.ts +75 -20
- package/extensions/workflows/model.ts +5 -1
- package/extensions/workflows/progress-projection.ts +7 -1
- package/extensions/workflows/prompt.ts +4 -10
- package/extensions/workflows/result-delivery.ts +96 -22
- package/extensions/workflows/retention.ts +6 -0
- package/extensions/workflows/runner.ts +11 -233
- package/extensions/workflows/sandbox.ts +4 -0
- package/package.json +7 -7
- package/skills/subagents/REFERENCE.md +9 -9
- package/skills/subagents/SKILL.md +2 -1
- package/skills/workflows/REFERENCE.md +5 -3
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +3 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/web-host.ts +330 -50
- package/web/protocol/types.ts +5 -0
- package/web/runtime/pi-runtime.ts +240 -25
- package/web/runtime/types.ts +32 -1
- package/web/ui/app.js +343 -41
- package/web/ui/index.html +3 -0
- package/web/ui/styles.css +119 -37
|
@@ -92,6 +92,8 @@ export interface AgentType {
|
|
|
92
92
|
readonly description: string;
|
|
93
93
|
/** Omitted = the child keeps the normal tool set. Present = allowlist. */
|
|
94
94
|
readonly tools?: readonly string[];
|
|
95
|
+
/** Only built-in investigator definitions carry this planning compatibility. */
|
|
96
|
+
readonly planningCompatible?: boolean;
|
|
95
97
|
/** "provider/model-id" or a bare id; resolved by the pi backend. */
|
|
96
98
|
readonly model?: string;
|
|
97
99
|
readonly reasoningEffort?: ReasoningEffort;
|
|
@@ -121,9 +123,9 @@ export const READ_ONLY_AGENT_TOOLS = [
|
|
|
121
123
|
export const BUILT_IN_AGENT_TYPES: readonly AgentType[] = [
|
|
122
124
|
{
|
|
123
125
|
name: "explorer",
|
|
126
|
+
planningCompatible: true,
|
|
124
127
|
description:
|
|
125
128
|
"Read-only codebase exploration. Usually use moderate reasoning, increasing it for harder tasks.",
|
|
126
|
-
tools: READ_ONLY_AGENT_TOOLS,
|
|
127
129
|
body: "Explore the codebase read-only. Trace the real flow, inspect related callers, and report concise evidence with file paths and line references.",
|
|
128
130
|
source: "built-in:explorer",
|
|
129
131
|
},
|
|
@@ -131,36 +133,22 @@ export const BUILT_IN_AGENT_TYPES: readonly AgentType[] = [
|
|
|
131
133
|
name: "implementer",
|
|
132
134
|
description:
|
|
133
135
|
"Focused implementation with repository checks. Usually use medium-high reasoning, adjusted for scope, risk, and task difficulty.",
|
|
134
|
-
tools: [
|
|
135
|
-
"read",
|
|
136
|
-
"bash",
|
|
137
|
-
"edit",
|
|
138
|
-
"write",
|
|
139
|
-
"grep",
|
|
140
|
-
"find",
|
|
141
|
-
"ls",
|
|
142
|
-
"fd",
|
|
143
|
-
"rg",
|
|
144
|
-
"git_show",
|
|
145
|
-
"git_diff",
|
|
146
|
-
"git_log",
|
|
147
|
-
],
|
|
148
136
|
body: "Implement the requested change carefully. Trace the affected flow first, make the smallest correct edit, and run relevant checks before reporting results.",
|
|
149
137
|
source: "built-in:implementer",
|
|
150
138
|
},
|
|
151
139
|
{
|
|
152
140
|
name: "reviewer",
|
|
141
|
+
planningCompatible: true,
|
|
153
142
|
description:
|
|
154
143
|
"Read-only review for correctness, safety, and regressions. Usually use high reasoning, adjusted for task difficulty.",
|
|
155
|
-
tools: READ_ONLY_AGENT_TOOLS,
|
|
156
144
|
body: "Review the requested code or change read-only. Identify concrete correctness, security, and regression risks with evidence; do not modify files.",
|
|
157
145
|
source: "built-in:reviewer",
|
|
158
146
|
},
|
|
159
147
|
{
|
|
160
148
|
name: "advisor",
|
|
149
|
+
planningCompatible: true,
|
|
161
150
|
description:
|
|
162
151
|
"Deep read-only analysis and technical advice. Usually use high reasoning, adjusted for task difficulty.",
|
|
163
|
-
tools: READ_ONLY_AGENT_TOOLS,
|
|
164
152
|
body: "Analyze the problem deeply without modifying files. Explain the relevant tradeoffs, risks, and recommended next step using repository evidence.",
|
|
165
153
|
source: "built-in:advisor",
|
|
166
154
|
},
|
|
@@ -19,11 +19,13 @@ 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";
|
|
28
|
+
import { toolPreview } from "./tool-preview.ts";
|
|
27
29
|
import type {
|
|
28
30
|
SubagentBackend,
|
|
29
31
|
SubagentCleanupReceipt,
|
|
@@ -49,6 +51,14 @@ import {
|
|
|
49
51
|
reclaimWorktree,
|
|
50
52
|
} from "../../../shared/worktree.ts";
|
|
51
53
|
import { AgentToolRenderLedger } from "../../../shared/agent-tool-renderer.ts";
|
|
54
|
+
import {
|
|
55
|
+
childToolsWithStructuredOutput,
|
|
56
|
+
createStructuredOutputTool,
|
|
57
|
+
encodeStructuredResult,
|
|
58
|
+
type EncodedStructuredResult,
|
|
59
|
+
STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION,
|
|
60
|
+
} from "../../../shared/structured-output.ts";
|
|
61
|
+
import { persistStructuredResultArtifact } from "../result-artifact.ts";
|
|
52
62
|
|
|
53
63
|
const DIRECT_WORKTREE_CLEANUP_TIMEOUT_MS = 4_000;
|
|
54
64
|
const PARTIAL_TEXT_MAX_LENGTH = 128 * 1_024;
|
|
@@ -109,27 +119,6 @@ function safeJson(value: unknown): string | undefined {
|
|
|
109
119
|
}
|
|
110
120
|
}
|
|
111
121
|
|
|
112
|
-
/** First non-empty line of a tool result-ish value (v1 liveToolPreview). */
|
|
113
|
-
function toolPreview(value: unknown): string | undefined {
|
|
114
|
-
if (typeof value === "string") {
|
|
115
|
-
return value
|
|
116
|
-
.split("\n")
|
|
117
|
-
.find((line) => line.trim())
|
|
118
|
-
?.trim();
|
|
119
|
-
}
|
|
120
|
-
if (!value || typeof value !== "object") return undefined;
|
|
121
|
-
const content = (value as { content?: unknown }).content;
|
|
122
|
-
if (!Array.isArray(content)) return undefined;
|
|
123
|
-
for (const part of content) {
|
|
124
|
-
if (!part || typeof part !== "object") continue;
|
|
125
|
-
const record = part as { type?: unknown; text?: unknown };
|
|
126
|
-
if (record.type !== "text" || typeof record.text !== "string") continue;
|
|
127
|
-
const firstLine = record.text.split("\n").find((line) => line.trim());
|
|
128
|
-
if (firstLine) return firstLine.trim();
|
|
129
|
-
}
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
122
|
function assistantParts(msg: AssistantMessage): TranscriptPart[] {
|
|
134
123
|
const parts: TranscriptPart[] = [];
|
|
135
124
|
for (const part of msg.content) {
|
|
@@ -198,38 +187,93 @@ const makePiSession = (
|
|
|
198
187
|
const thinkingLevel = (task.reasoningEffort ??
|
|
199
188
|
task.parent.inheritedThinkingLevel) as ThinkingLevel | undefined;
|
|
200
189
|
|
|
190
|
+
let capturedStructured: EncodedStructuredResult | undefined;
|
|
191
|
+
const structuredOutputTool =
|
|
192
|
+
task.outputSchema === undefined
|
|
193
|
+
? undefined
|
|
194
|
+
: createStructuredOutputTool(task.outputSchema, (value) => {
|
|
195
|
+
capturedStructured = encodeStructuredResult(value);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Own the session before asynchronous startup. Interruption can happen
|
|
199
|
+
// before the normal backend finalizer has been installed.
|
|
200
|
+
let acquiringSession: AgentSession | undefined;
|
|
201
|
+
let startupOwned = true;
|
|
202
|
+
const cleanupStartup = () =>
|
|
203
|
+
acquiringSession
|
|
204
|
+
? shutdownAndDisposeChildSession(acquiringSession, {
|
|
205
|
+
abort: true,
|
|
206
|
+
timeoutMs: options.shutdownTimeoutMs,
|
|
207
|
+
})
|
|
208
|
+
: Promise.resolve();
|
|
209
|
+
yield* Effect.addFinalizer(() =>
|
|
210
|
+
Effect.promise(async () => {
|
|
211
|
+
if (startupOwned) await cleanupStartup();
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
|
|
201
215
|
const session = yield* Effect.tryPromise({
|
|
202
|
-
try: async () => {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
options.sessionFactory ?? createAgentSession
|
|
212
|
-
)({
|
|
213
|
-
cwd: task.cwd,
|
|
214
|
-
sessionManager: SessionManager.create(task.cwd),
|
|
215
|
-
settingsManager,
|
|
216
|
-
resourceLoader: loader,
|
|
217
|
-
model,
|
|
218
|
-
thinkingLevel,
|
|
219
|
-
...childToolPolicy(task.tools),
|
|
220
|
-
});
|
|
221
|
-
// Start child extension session hooks/resources in headless mode.
|
|
222
|
-
// A rejection here would otherwise leak the freshly created session:
|
|
223
|
-
// the scope finalizer that owns cleanup is only registered later.
|
|
216
|
+
try: async (signal) => {
|
|
217
|
+
const checkCancelled = () => {
|
|
218
|
+
if (signal.aborted)
|
|
219
|
+
throw signal.reason ?? new Error("Subagent startup cancelled");
|
|
220
|
+
};
|
|
221
|
+
const onCancelled = () => {
|
|
222
|
+
void cleanupStartup().catch(() => {});
|
|
223
|
+
};
|
|
224
|
+
signal.addEventListener("abort", onCancelled, { once: true });
|
|
224
225
|
try {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
226
|
+
checkCancelled();
|
|
227
|
+
const appendSystemPrompt = [
|
|
228
|
+
...(task.appendSystemPrompt ?? []),
|
|
229
|
+
...(structuredOutputTool
|
|
230
|
+
? [STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION]
|
|
231
|
+
: []),
|
|
232
|
+
];
|
|
233
|
+
const { loader, settingsManager } = await createChildResources({
|
|
234
|
+
cwd: task.cwd,
|
|
235
|
+
projectTrusted: task.parent.projectTrusted,
|
|
236
|
+
...(appendSystemPrompt.length > 0 ? { appendSystemPrompt } : {}),
|
|
237
|
+
});
|
|
238
|
+
checkCancelled();
|
|
239
|
+
const { session } = await (
|
|
240
|
+
options.sessionFactory ?? createAgentSession
|
|
241
|
+
)({
|
|
242
|
+
cwd: task.cwd,
|
|
243
|
+
sessionManager: SessionManager.create(task.cwd),
|
|
244
|
+
settingsManager,
|
|
245
|
+
resourceLoader: loader,
|
|
246
|
+
model,
|
|
247
|
+
thinkingLevel,
|
|
248
|
+
...(structuredOutputTool
|
|
249
|
+
? { customTools: [structuredOutputTool] }
|
|
250
|
+
: {}),
|
|
251
|
+
...childToolPolicy(
|
|
252
|
+
childToolsWithStructuredOutput(
|
|
253
|
+
task.tools,
|
|
254
|
+
structuredOutputTool !== undefined,
|
|
255
|
+
),
|
|
256
|
+
),
|
|
229
257
|
});
|
|
258
|
+
acquiringSession = session;
|
|
259
|
+
checkCancelled();
|
|
260
|
+
// Never start extension binding for a factory that completed after
|
|
261
|
+
// cancellation. Already-running hooks retain bounded cleanup ownership.
|
|
262
|
+
await bindChildSessionExtensions(
|
|
263
|
+
session,
|
|
264
|
+
childToolsWithStructuredOutput(
|
|
265
|
+
task.tools,
|
|
266
|
+
structuredOutputTool !== undefined,
|
|
267
|
+
),
|
|
268
|
+
);
|
|
269
|
+
checkCancelled();
|
|
270
|
+
return session;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
await cleanupStartup();
|
|
230
273
|
throw error;
|
|
274
|
+
} finally {
|
|
275
|
+
signal.removeEventListener("abort", onCancelled);
|
|
231
276
|
}
|
|
232
|
-
return session;
|
|
233
277
|
},
|
|
234
278
|
catch: (error) => new SpawnError({ message: boundedError(error) }),
|
|
235
279
|
});
|
|
@@ -338,11 +382,46 @@ const makePiSession = (
|
|
|
338
382
|
});
|
|
339
383
|
return;
|
|
340
384
|
}
|
|
385
|
+
if (task.outputSchema !== undefined && capturedStructured === undefined) {
|
|
386
|
+
emit({
|
|
387
|
+
_tag: "RunSettled",
|
|
388
|
+
outcome: {
|
|
389
|
+
_tag: "Failed",
|
|
390
|
+
errorText:
|
|
391
|
+
"Agent finished without calling structured_output; no structured result matching output_schema was produced.",
|
|
392
|
+
partialText,
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
let structuredResult;
|
|
398
|
+
if (capturedStructured) {
|
|
399
|
+
try {
|
|
400
|
+
structuredResult = {
|
|
401
|
+
...capturedStructured,
|
|
402
|
+
artifactPath: persistStructuredResultArtifact(
|
|
403
|
+
getAgentDir(),
|
|
404
|
+
capturedStructured.json,
|
|
405
|
+
),
|
|
406
|
+
};
|
|
407
|
+
} catch (error) {
|
|
408
|
+
emit({
|
|
409
|
+
_tag: "RunSettled",
|
|
410
|
+
outcome: {
|
|
411
|
+
_tag: "Failed",
|
|
412
|
+
errorText: `Structured result artifact could not be persisted: ${boundedError(error)}`,
|
|
413
|
+
partialText,
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
341
419
|
emit({
|
|
342
420
|
_tag: "RunSettled",
|
|
343
421
|
outcome: {
|
|
344
422
|
_tag: "Completed",
|
|
345
423
|
finalText: prompt.finalText,
|
|
424
|
+
...(structuredResult ? { structuredResult } : {}),
|
|
346
425
|
},
|
|
347
426
|
});
|
|
348
427
|
};
|
|
@@ -580,6 +659,8 @@ const makePiSession = (
|
|
|
580
659
|
}),
|
|
581
660
|
);
|
|
582
661
|
|
|
662
|
+
startupOwned = false;
|
|
663
|
+
|
|
583
664
|
/** Start a fresh run (v1 manager.run): fire-and-forget, errors -> events. */
|
|
584
665
|
const startRun = (text: string) => {
|
|
585
666
|
if (state.activePrompt) {
|
|
@@ -597,6 +678,7 @@ const makePiSession = (
|
|
|
597
678
|
promise: Promise.resolve(),
|
|
598
679
|
};
|
|
599
680
|
state.activePrompt = activePrompt;
|
|
681
|
+
capturedStructured = undefined;
|
|
600
682
|
state.settled = false;
|
|
601
683
|
emit({ _tag: "RunStarted" });
|
|
602
684
|
let prompt: Promise<void>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Match the manager's existing transcript text limit; canonical Pi tool results
|
|
2
|
+
// remain intact. Only the normalized event's single-line preview is bounded.
|
|
3
|
+
const TOOL_PREVIEW_MAX_LENGTH = 64 * 1_024;
|
|
4
|
+
|
|
5
|
+
function firstMeaningfulLine(text: string) {
|
|
6
|
+
// Search only until the first non-whitespace character. Unlike splitting the
|
|
7
|
+
// entire log, this preserves blank-line behavior without visiting its tail.
|
|
8
|
+
const start = text.search(/\S/);
|
|
9
|
+
if (start < 0) return undefined;
|
|
10
|
+
const prefix = text.slice(start, start + TOOL_PREVIEW_MAX_LENGTH);
|
|
11
|
+
const newline = prefix.indexOf("\n");
|
|
12
|
+
return (newline < 0 ? prefix : prefix.slice(0, newline)).trimEnd();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** First meaningful line of a tool result, without splitting accumulated logs. */
|
|
16
|
+
export function toolPreview(value: unknown) {
|
|
17
|
+
if (typeof value === "string") return firstMeaningfulLine(value);
|
|
18
|
+
if (!value || typeof value !== "object") return undefined;
|
|
19
|
+
const content = (value as { content?: unknown }).content;
|
|
20
|
+
if (!Array.isArray(content)) return undefined;
|
|
21
|
+
for (const part of content) {
|
|
22
|
+
if (!part || typeof part !== "object") continue;
|
|
23
|
+
const record = part as { type?: unknown; text?: unknown };
|
|
24
|
+
if (record.type !== "text" || typeof record.text !== "string") continue;
|
|
25
|
+
const firstLine = firstMeaningfulLine(record.text);
|
|
26
|
+
if (firstLine) return firstLine;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
@@ -69,6 +69,8 @@ export interface SpawnTask {
|
|
|
69
69
|
readonly tools?: readonly string[];
|
|
70
70
|
/** Agent type that supplied the above, for the session label. */
|
|
71
71
|
readonly agentTypeName?: string;
|
|
72
|
+
/** Optional JSON Schema for one terminating, validated child result. */
|
|
73
|
+
readonly outputSchema?: unknown;
|
|
72
74
|
/**
|
|
73
75
|
* Isolated git worktree this child runs in, created by the tool layer. The
|
|
74
76
|
* backend only reclaims it when the session scope closes; it does not know
|
|
@@ -142,7 +144,11 @@ export interface QueuedMessage {
|
|
|
142
144
|
// --- Events ------------------------------------------------------------------
|
|
143
145
|
|
|
144
146
|
export type RunOutcome =
|
|
145
|
-
| {
|
|
147
|
+
| {
|
|
148
|
+
readonly _tag: "Completed";
|
|
149
|
+
readonly finalText: string;
|
|
150
|
+
readonly structuredResult?: StructuredSubagentResult;
|
|
151
|
+
}
|
|
146
152
|
| {
|
|
147
153
|
readonly _tag: "Failed";
|
|
148
154
|
readonly errorText: string;
|
|
@@ -232,10 +238,19 @@ export interface SubagentSnapshot {
|
|
|
232
238
|
readonly queued: ReadonlyArray<QueuedMessage>;
|
|
233
239
|
/** Final text of the most recent completed run (v1 `finalOutput`). */
|
|
234
240
|
readonly finalText: string;
|
|
241
|
+
/** Present only when this run supplied and satisfied output_schema. */
|
|
242
|
+
readonly structuredResult?: StructuredSubagentResult;
|
|
235
243
|
/** Count of finalized assistant messages (for subagent_check). */
|
|
236
244
|
readonly turns: number;
|
|
237
245
|
}
|
|
238
246
|
|
|
247
|
+
export interface StructuredSubagentResult {
|
|
248
|
+
readonly value: unknown;
|
|
249
|
+
readonly json: string;
|
|
250
|
+
readonly byteLength: number;
|
|
251
|
+
readonly artifactPath: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
239
254
|
/** Final text, or the live streaming buffer while a run is active (v1 `latestOutput`). */
|
|
240
255
|
export function latestText(snap: SubagentSnapshot) {
|
|
241
256
|
const live = snap.liveAssistant?.text.trim();
|
|
@@ -10,10 +10,8 @@
|
|
|
10
10
|
* imperative TUI components (which render synchronously) can read snapshots
|
|
11
11
|
* and issue fire-and-forget commands without touching the Effect runtime.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* failure (releasing its concurrency slot) instead of hanging forever,
|
|
16
|
-
* mirroring the workflow runner's watchdog.
|
|
13
|
+
* Pi owns provider transport timeouts and retries. This manager owns explicit
|
|
14
|
+
* cancellation, settlement, and bounded cleanup, not model-output deadlines.
|
|
17
15
|
*/
|
|
18
16
|
|
|
19
17
|
import {
|
|
@@ -60,13 +58,6 @@ export const MAX_TRACKED = 64;
|
|
|
60
58
|
const STOP_TIMEOUT_MS = 5_000;
|
|
61
59
|
/** Session abort/shutdown (5s) plus bounded direct-worktree cleanup (4s). */
|
|
62
60
|
const ENTRY_CLOSE_TIMEOUT_MS = 10_000;
|
|
63
|
-
/**
|
|
64
|
-
* First-response watchdog: a run whose provider accepts the request but
|
|
65
|
-
* never emits an assistant event is settled as a failure so it cannot
|
|
66
|
-
* occupy a concurrency slot forever. Matches the workflow runner's
|
|
67
|
-
* MODEL_PROGRESS_TIMEOUT_MS (extensions/workflows/runner.ts).
|
|
68
|
-
*/
|
|
69
|
-
export const FIRST_RESPONSE_TIMEOUT_MS = 45_000;
|
|
70
61
|
const ERROR_TEXT_MAX_LENGTH = 4_096;
|
|
71
62
|
const TRANSCRIPT_TEXT_MAX_LENGTH = 64 * 1_024;
|
|
72
63
|
const LIVE_ASSISTANT_MAX_LENGTH = 128 * 1_024;
|
|
@@ -77,10 +68,6 @@ function bounded(text: string) {
|
|
|
77
68
|
return text.slice(0, ERROR_TEXT_MAX_LENGTH);
|
|
78
69
|
}
|
|
79
70
|
|
|
80
|
-
function formatWatchdogTimeout(ms: number) {
|
|
81
|
-
return ms % 1_000 === 0 ? `${ms / 1_000} seconds` : `${ms} ms`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
71
|
function boundedTranscriptText(text: string) {
|
|
85
72
|
return text.slice(0, TRANSCRIPT_TEXT_MAX_LENGTH);
|
|
86
73
|
}
|
|
@@ -123,6 +110,7 @@ interface MutableSnapshot {
|
|
|
123
110
|
liveTools: LiveToolState[];
|
|
124
111
|
queued: SubagentSnapshot["queued"];
|
|
125
112
|
finalText: string;
|
|
113
|
+
structuredResult?: SubagentSnapshot["structuredResult"];
|
|
126
114
|
turns: number;
|
|
127
115
|
}
|
|
128
116
|
|
|
@@ -132,8 +120,6 @@ interface Entry {
|
|
|
132
120
|
scope: Scope.Closeable;
|
|
133
121
|
pump?: Fiber.Fiber<void>;
|
|
134
122
|
liveToolMap: Map<string, LiveToolState>;
|
|
135
|
-
/** First-response watchdog timer for the active (or just-armed) run. */
|
|
136
|
-
watchdogTimer?: ReturnType<typeof setTimeout>;
|
|
137
123
|
/** Idle restart dispatched but RunStarted not folded yet; counts as running
|
|
138
124
|
* so concurrent restarts cannot race past the cap. */
|
|
139
125
|
restarting?: boolean;
|
|
@@ -213,8 +199,6 @@ export class SubagentManager extends Context.Service<
|
|
|
213
199
|
|
|
214
200
|
const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
215
201
|
Effect.gen(function* () {
|
|
216
|
-
const firstResponseTimeoutMs =
|
|
217
|
-
config.firstResponseTimeoutMs ?? FIRST_RESPONSE_TIMEOUT_MS;
|
|
218
202
|
const registry = yield* BackendRegistry;
|
|
219
203
|
// Detached forker for sync contexts (read-model commands, pruning) that
|
|
220
204
|
// preserves the manager's services instead of using the global runtime.
|
|
@@ -343,7 +327,6 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
343
327
|
};
|
|
344
328
|
|
|
345
329
|
const settle = (entry: Entry, outcome: RunOutcome) => {
|
|
346
|
-
clearWatchdog(entry);
|
|
347
330
|
const s = entry.snapshot;
|
|
348
331
|
const wasRestarting = entry.restarting === true;
|
|
349
332
|
entry.restarting = false;
|
|
@@ -363,6 +346,7 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
363
346
|
s.outcome = "completed";
|
|
364
347
|
s.errorText = undefined;
|
|
365
348
|
s.finalText = outcome.finalText.slice(0, FINAL_TEXT_MAX_LENGTH);
|
|
349
|
+
s.structuredResult = outcome.structuredResult;
|
|
366
350
|
break;
|
|
367
351
|
case "Failed":
|
|
368
352
|
s.status = "error";
|
|
@@ -373,6 +357,7 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
373
357
|
0,
|
|
374
358
|
FINAL_TEXT_MAX_LENGTH,
|
|
375
359
|
);
|
|
360
|
+
s.structuredResult = undefined;
|
|
376
361
|
break;
|
|
377
362
|
case "Interrupted":
|
|
378
363
|
s.status = "error";
|
|
@@ -382,6 +367,7 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
382
367
|
0,
|
|
383
368
|
FINAL_TEXT_MAX_LENGTH,
|
|
384
369
|
);
|
|
370
|
+
s.structuredResult = undefined;
|
|
385
371
|
break;
|
|
386
372
|
}
|
|
387
373
|
s.liveAssistant = undefined;
|
|
@@ -399,44 +385,6 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
399
385
|
pruneSettled();
|
|
400
386
|
};
|
|
401
387
|
|
|
402
|
-
/** Stop the first-response watchdog (first response arrived / run settled). */
|
|
403
|
-
const clearWatchdog = (entry: Entry) => {
|
|
404
|
-
if (entry.watchdogTimer !== undefined) {
|
|
405
|
-
clearTimeout(entry.watchdogTimer);
|
|
406
|
-
entry.watchdogTimer = undefined;
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
/** Settle a run whose provider never emitted a first assistant response. */
|
|
411
|
-
const watchdogExpired = (entry: Entry) => {
|
|
412
|
-
entry.watchdogTimer = undefined;
|
|
413
|
-
if (!isBusy(entry)) return;
|
|
414
|
-
const model = entry.snapshot.meta.modelLabel;
|
|
415
|
-
settle(entry, {
|
|
416
|
-
_tag: "Failed",
|
|
417
|
-
errorText: `Agent received no assistant response event${model ? ` for ${model}` : ""} within ${formatWatchdogTimeout(firstResponseTimeoutMs)}; the provider request may be stalled. Retry the subagent.`,
|
|
418
|
-
});
|
|
419
|
-
// The stalled session cannot be trusted to abort cooperatively; dispose
|
|
420
|
-
// it like the abort-deadline path so it cannot revive into a zombie run.
|
|
421
|
-
const fiber = runDetached(
|
|
422
|
-
closeEntryScope(entry).pipe(
|
|
423
|
-
Effect.timeout(ENTRY_CLOSE_TIMEOUT_MS),
|
|
424
|
-
Effect.ignore,
|
|
425
|
-
),
|
|
426
|
-
);
|
|
427
|
-
cleanups.add(fiber);
|
|
428
|
-
fiber.addObserver(() => cleanups.delete(fiber));
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
/** Arm the first-response watchdog for the entry's current run. */
|
|
432
|
-
const armWatchdog = (entry: Entry) => {
|
|
433
|
-
clearWatchdog(entry);
|
|
434
|
-
entry.watchdogTimer = setTimeout(
|
|
435
|
-
() => watchdogExpired(entry),
|
|
436
|
-
firstResponseTimeoutMs,
|
|
437
|
-
);
|
|
438
|
-
};
|
|
439
|
-
|
|
440
388
|
const foldEvent = (entry: Entry, event: SubagentEvent) => {
|
|
441
389
|
const s = entry.snapshot;
|
|
442
390
|
switch (event._tag) {
|
|
@@ -446,7 +394,7 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
446
394
|
s.outcome = undefined;
|
|
447
395
|
s.settledAt = undefined;
|
|
448
396
|
s.errorText = undefined;
|
|
449
|
-
|
|
397
|
+
s.structuredResult = undefined;
|
|
450
398
|
break;
|
|
451
399
|
case "RunSettled":
|
|
452
400
|
settle(entry, event.outcome);
|
|
@@ -458,7 +406,6 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
458
406
|
});
|
|
459
407
|
break;
|
|
460
408
|
case "AssistantDelta": {
|
|
461
|
-
clearWatchdog(entry);
|
|
462
409
|
const live = s.liveAssistant ?? { text: "", thinking: "" };
|
|
463
410
|
s.liveAssistant =
|
|
464
411
|
event.kind === "text"
|
|
@@ -477,7 +424,6 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
477
424
|
break;
|
|
478
425
|
}
|
|
479
426
|
case "AssistantMessage":
|
|
480
|
-
clearWatchdog(entry);
|
|
481
427
|
appendTranscript(s, {
|
|
482
428
|
kind: "assistant",
|
|
483
429
|
parts: event.parts.map((part) =>
|
|
@@ -623,9 +569,6 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
623
569
|
liveToolMap: new Map(),
|
|
624
570
|
};
|
|
625
571
|
entries.set(id, entry);
|
|
626
|
-
// The run is live from the caller's perspective before RunStarted
|
|
627
|
-
// reaches the pump; guard that window too.
|
|
628
|
-
armWatchdog(entry);
|
|
629
572
|
|
|
630
573
|
// Pump: fold the event stream into the snapshot. Tied to the entry
|
|
631
574
|
// scope, so closing the scope stops it. If the stream ends while the
|
|
@@ -776,10 +719,6 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
776
719
|
// both pass the check in that window. Cleared by RunStarted/settle,
|
|
777
720
|
// or here when the backend rejects the send.
|
|
778
721
|
entry.restarting = true;
|
|
779
|
-
// A backend that accepts the send but never starts the run would
|
|
780
|
-
// hold the slot forever; guard the restart window the same way the
|
|
781
|
-
// spawn path guards its pre-RunStarted window.
|
|
782
|
-
armWatchdog(entry);
|
|
783
722
|
return entry.session.send(text).pipe(
|
|
784
723
|
Effect.onError(() =>
|
|
785
724
|
Effect.sync(() => {
|
|
@@ -795,7 +734,6 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
795
734
|
const disposeAll = Effect.gen(function* () {
|
|
796
735
|
disposed = true;
|
|
797
736
|
const all = [...entries.values()];
|
|
798
|
-
for (const entry of all) clearWatchdog(entry);
|
|
799
737
|
entries.clear();
|
|
800
738
|
yield* Effect.forEach(
|
|
801
739
|
all,
|
|
@@ -873,8 +811,6 @@ const makeManager = (config: SubagentManagerConfig = {}) =>
|
|
|
873
811
|
});
|
|
874
812
|
|
|
875
813
|
export interface SubagentManagerConfig {
|
|
876
|
-
/** Test-only override for the first-response watchdog timeout. */
|
|
877
|
-
firstResponseTimeoutMs?: number;
|
|
878
814
|
/** Session-branch high-water marks restored by the extension host. */
|
|
879
815
|
initialModelCounter?: number;
|
|
880
816
|
initialBtwCounter?: number;
|
|
@@ -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) {
|
|
@@ -141,17 +141,18 @@ export const SUBAGENT_SPAWN_PROMPT_GUIDELINES = [
|
|
|
141
141
|
/** Model-facing schema descriptions for subagent_spawn task and execution options. */
|
|
142
142
|
export const SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS = {
|
|
143
143
|
prompt:
|
|
144
|
-
"
|
|
144
|
+
"Self-contained task: include needed context, file paths, and expected report.",
|
|
145
145
|
name: "Short human-readable name shown in listings and the UI",
|
|
146
146
|
harness: 'Optional; "pi" is the only harness and the default.',
|
|
147
147
|
workingDir:
|
|
148
|
-
"
|
|
148
|
+
"Child cwd, absolute or relative to parent. Target project trust is checked separately.",
|
|
149
149
|
isolation:
|
|
150
150
|
'Use "worktree" for concurrent writers and tell the child to commit. See the Subagents Skill for lifecycle details.',
|
|
151
151
|
model:
|
|
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
|
);
|
|
@@ -77,6 +77,38 @@ export function persistResultArtifact(agentDir: string, content: string) {
|
|
|
77
77
|
return artifactPath;
|
|
78
78
|
}
|
|
79
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
|
+
|
|
80
112
|
/**
|
|
81
113
|
* Build the single model-visible projection used by automatic delivery and
|
|
82
114
|
* explicit waits. Short answers pass through byte-for-byte. Long answers keep
|