@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
|
@@ -173,9 +173,6 @@ function packageSourceValue(source: PackageSource) {
|
|
|
173
173
|
|
|
174
174
|
const CHILD_DISABLED_OPENPI_EXTENSION =
|
|
175
175
|
"-extensions/git-info/index.ts" as const;
|
|
176
|
-
const OPENPI_GIT_INFO_EXTENSION_PATH = realpathSync.native(
|
|
177
|
-
fileURLToPath(new URL("../git-info/index.ts", import.meta.url)),
|
|
178
|
-
);
|
|
179
176
|
|
|
180
177
|
function canonicalExistingPath(value: string) {
|
|
181
178
|
try {
|
|
@@ -185,15 +182,38 @@ function canonicalExistingPath(value: string) {
|
|
|
185
182
|
}
|
|
186
183
|
}
|
|
187
184
|
|
|
185
|
+
/**
|
|
186
|
+
* ENOENT means git-info is truly absent (trimmed fork, partial install):
|
|
187
|
+
* nothing to exclude. Any other failure (permissions, symlink loops) means we
|
|
188
|
+
* cannot verify the Git-polling extension is present, so fail closed instead
|
|
189
|
+
* of running a child with an unverifiable extension.
|
|
190
|
+
*/
|
|
191
|
+
export function resolveGitInfoPathOrThrow(
|
|
192
|
+
resolve: (value: string) => string = realpathSync.native,
|
|
193
|
+
): string | undefined {
|
|
194
|
+
try {
|
|
195
|
+
return resolve(
|
|
196
|
+
fileURLToPath(new URL("../git-info/index.ts", import.meta.url)),
|
|
197
|
+
);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
188
204
|
function excludeOpenPiGitInfoExtension(
|
|
189
205
|
resources: LoadExtensionsResult,
|
|
206
|
+
resolve: (value: string) => string = realpathSync.native,
|
|
190
207
|
): LoadExtensionsResult {
|
|
208
|
+
const gitInfoPath = resolveGitInfoPathOrThrow(resolve);
|
|
209
|
+
// undefined = absent (ENOENT): nothing to exclude. Non-ENOENT failures
|
|
210
|
+
// throw from resolveGitInfoPathOrThrow and fail child creation upstream.
|
|
211
|
+
if (gitInfoPath === undefined) return resources;
|
|
191
212
|
return {
|
|
192
213
|
...resources,
|
|
193
214
|
extensions: resources.extensions.filter(
|
|
194
215
|
(extension) =>
|
|
195
|
-
canonicalExistingPath(extension.resolvedPath) !==
|
|
196
|
-
OPENPI_GIT_INFO_EXTENSION_PATH,
|
|
216
|
+
canonicalExistingPath(extension.resolvedPath) !== gitInfoPath,
|
|
197
217
|
),
|
|
198
218
|
};
|
|
199
219
|
}
|
|
@@ -530,6 +550,20 @@ export function effectiveChildToolAllowlist(tools?: readonly string[]) {
|
|
|
530
550
|
);
|
|
531
551
|
}
|
|
532
552
|
|
|
553
|
+
/** Project the parent's active surface into a child; a role can only narrow it.
|
|
554
|
+
* Active tools are a visibility choice, not a filesystem/network sandbox.
|
|
555
|
+
* Inactive tools are not implicitly activated by delegation.
|
|
556
|
+
*/
|
|
557
|
+
export function inheritedChildToolAllowlist(
|
|
558
|
+
parentTools: readonly string[],
|
|
559
|
+
roleTools?: readonly string[],
|
|
560
|
+
) {
|
|
561
|
+
const allowed = roleTools === undefined ? undefined : new Set(roleTools);
|
|
562
|
+
return effectiveChildToolAllowlist([...new Set(parentTools)])!.filter(
|
|
563
|
+
(name) => allowed === undefined || allowed.has(name),
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
533
567
|
export function childToolPolicy(tools?: readonly string[]) {
|
|
534
568
|
const effectiveTools = effectiveChildToolAllowlist(tools);
|
|
535
569
|
return {
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
export type CompletionProducer = "subagent" | "workflow" | "background";
|
|
2
|
+
export type CompletionWakePolicy =
|
|
3
|
+
| "follow-up"
|
|
4
|
+
| "next-turn"
|
|
5
|
+
| "producer-policy";
|
|
6
|
+
|
|
7
|
+
export interface CompletionOwner {
|
|
8
|
+
readonly sessionId: string;
|
|
9
|
+
readonly epoch: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CompletionSessionIdentity {
|
|
13
|
+
getSessionId(): string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Transport metadata only; producer state remains the terminal authority. */
|
|
17
|
+
export interface CompletionEnvelope<T> {
|
|
18
|
+
readonly deliveryId: string;
|
|
19
|
+
readonly owner: CompletionOwner;
|
|
20
|
+
readonly producer: CompletionProducer;
|
|
21
|
+
readonly producerId: string;
|
|
22
|
+
readonly terminalRef: unknown;
|
|
23
|
+
readonly wake: CompletionWakePolicy;
|
|
24
|
+
readonly payload: T;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CompletionDeadLetter {
|
|
28
|
+
readonly deliveryId: string;
|
|
29
|
+
readonly producer: CompletionProducer;
|
|
30
|
+
readonly producerId: string;
|
|
31
|
+
readonly failure: "owner-unavailable" | "stale-owner";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sameOwner(left: CompletionOwner, right: CompletionOwner) {
|
|
35
|
+
return (
|
|
36
|
+
Boolean(left.sessionId) &&
|
|
37
|
+
left.sessionId !== "unowned" &&
|
|
38
|
+
left.sessionId === right.sessionId &&
|
|
39
|
+
left.epoch === right.epoch
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let nextOwnerEpoch = 1;
|
|
44
|
+
const ownerBySessionIdentity = new WeakMap<object, CompletionOwner>();
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Bind one process-local generation to a Pi SessionManager identity.
|
|
48
|
+
*
|
|
49
|
+
* All OpenPI producers observing the same manager share an owner. A replaced
|
|
50
|
+
* manager, or a manager whose Session id changes, receives a new epoch so late
|
|
51
|
+
* callbacks cannot target the replacement transcript.
|
|
52
|
+
*/
|
|
53
|
+
export function completionOwnerFor(
|
|
54
|
+
identity: CompletionSessionIdentity,
|
|
55
|
+
): CompletionOwner {
|
|
56
|
+
const sessionId = identity.getSessionId();
|
|
57
|
+
const existing = ownerBySessionIdentity.get(identity);
|
|
58
|
+
if (existing?.sessionId === sessionId) return existing;
|
|
59
|
+
const owner = { sessionId, epoch: nextOwnerEpoch++ };
|
|
60
|
+
ownerBySessionIdentity.set(identity, owner);
|
|
61
|
+
return owner;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* One atomic consumption gate shared by all background producers.
|
|
66
|
+
*
|
|
67
|
+
* Claim removes before transport; a failed transport retries the exact
|
|
68
|
+
* envelopes. A successful transport leaves them consumed. No execution facts
|
|
69
|
+
* or result bytes are stored here.
|
|
70
|
+
*/
|
|
71
|
+
export function createCompletionInbox<T>() {
|
|
72
|
+
const pending = new Map<string, CompletionEnvelope<T>>();
|
|
73
|
+
const inFlight = new Map<string, CompletionEnvelope<T>>();
|
|
74
|
+
const deadLetters: CompletionDeadLetter[] = [];
|
|
75
|
+
|
|
76
|
+
const reject = (
|
|
77
|
+
envelope: CompletionEnvelope<T>,
|
|
78
|
+
failure: CompletionDeadLetter["failure"],
|
|
79
|
+
) => {
|
|
80
|
+
deadLetters.push({
|
|
81
|
+
deliveryId: envelope.deliveryId,
|
|
82
|
+
producer: envelope.producer,
|
|
83
|
+
producerId: envelope.producerId,
|
|
84
|
+
failure,
|
|
85
|
+
});
|
|
86
|
+
return false;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const admit = (
|
|
90
|
+
envelope: CompletionEnvelope<T>,
|
|
91
|
+
owner: CompletionOwner | undefined,
|
|
92
|
+
) => {
|
|
93
|
+
if (!owner) return reject(envelope, "owner-unavailable");
|
|
94
|
+
if (!sameOwner(envelope.owner, owner)) {
|
|
95
|
+
return reject(envelope, "stale-owner");
|
|
96
|
+
}
|
|
97
|
+
pending.set(envelope.deliveryId, envelope);
|
|
98
|
+
return true;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Restore a failed attempt ahead of completions that arrived meanwhile. */
|
|
102
|
+
const retry = (
|
|
103
|
+
envelopes: readonly CompletionEnvelope<T>[],
|
|
104
|
+
owner: CompletionOwner | undefined,
|
|
105
|
+
) => {
|
|
106
|
+
const current = [...pending.values()];
|
|
107
|
+
pending.clear();
|
|
108
|
+
for (const envelope of envelopes) {
|
|
109
|
+
inFlight.delete(envelope.deliveryId);
|
|
110
|
+
admit(envelope, owner);
|
|
111
|
+
}
|
|
112
|
+
for (const envelope of current) admit(envelope, owner);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
defer(envelope: CompletionEnvelope<T>, owner: CompletionOwner | undefined) {
|
|
117
|
+
return admit(envelope, owner);
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/** Explicit status/wait and automatic delivery atomically race here. */
|
|
121
|
+
consume(producer: CompletionProducer, producerIds: Iterable<string>) {
|
|
122
|
+
const ids = new Set(producerIds);
|
|
123
|
+
for (const [deliveryId, envelope] of pending) {
|
|
124
|
+
if (envelope.producer === producer && ids.has(envelope.producerId)) {
|
|
125
|
+
pending.delete(deliveryId);
|
|
126
|
+
inFlight.delete(deliveryId);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
consumeDeliveryIds(deliveryIds: Iterable<string>) {
|
|
132
|
+
for (const deliveryId of deliveryIds) {
|
|
133
|
+
pending.delete(deliveryId);
|
|
134
|
+
inFlight.delete(deliveryId);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
claim(
|
|
139
|
+
owner: CompletionOwner | undefined,
|
|
140
|
+
maximum = Number.POSITIVE_INFINITY,
|
|
141
|
+
) {
|
|
142
|
+
const claimed: CompletionEnvelope<T>[] = [];
|
|
143
|
+
for (const [deliveryId, envelope] of pending) {
|
|
144
|
+
if (claimed.length >= maximum) break;
|
|
145
|
+
pending.delete(deliveryId);
|
|
146
|
+
if (!owner) {
|
|
147
|
+
reject(envelope, "owner-unavailable");
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (!sameOwner(envelope.owner, owner)) {
|
|
151
|
+
reject(envelope, "stale-owner");
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
inFlight.set(deliveryId, envelope);
|
|
155
|
+
claimed.push(envelope);
|
|
156
|
+
}
|
|
157
|
+
return claimed;
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
retry,
|
|
161
|
+
|
|
162
|
+
retryClaimed(
|
|
163
|
+
producer: CompletionProducer,
|
|
164
|
+
producerIds: Iterable<string>,
|
|
165
|
+
owner: CompletionOwner | undefined,
|
|
166
|
+
) {
|
|
167
|
+
const ids = new Set(producerIds);
|
|
168
|
+
const envelopes = [...inFlight.values()].filter(
|
|
169
|
+
(envelope) =>
|
|
170
|
+
envelope.producer === producer && ids.has(envelope.producerId),
|
|
171
|
+
);
|
|
172
|
+
retry(envelopes, owner);
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
acknowledge(deliveryIds: Iterable<string>) {
|
|
176
|
+
for (const deliveryId of deliveryIds) inFlight.delete(deliveryId);
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
size() {
|
|
180
|
+
return pending.size;
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
inspectDeadLetters() {
|
|
184
|
+
return [...deadLetters];
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
clear() {
|
|
188
|
+
pending.clear();
|
|
189
|
+
inFlight.clear();
|
|
190
|
+
deadLetters.length = 0;
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -63,6 +63,9 @@ export type FooterLines = readonly (readonly FooterLayoutItem[])[];
|
|
|
63
63
|
export const DETAIL_DISPLAYS = ["full", "compact"] as const;
|
|
64
64
|
export type DetailDisplay = (typeof DETAIL_DISPLAYS)[number];
|
|
65
65
|
|
|
66
|
+
export const WEB_THEMES = ["system", "light", "dark"] as const;
|
|
67
|
+
export type WebTheme = (typeof WEB_THEMES)[number];
|
|
68
|
+
|
|
66
69
|
export const CAPABILITY_DISCOVERY_MODES = ["explicit", "adaptive"] as const;
|
|
67
70
|
export type CapabilityDiscoveryMode =
|
|
68
71
|
(typeof CAPABILITY_DISCOVERY_MODES)[number];
|
|
@@ -149,6 +152,7 @@ export interface MyPiSetupConfig {
|
|
|
149
152
|
readonly maxAgentCalls: number;
|
|
150
153
|
};
|
|
151
154
|
readonly ui: {
|
|
155
|
+
readonly webTheme: WebTheme;
|
|
152
156
|
readonly showHeader: boolean;
|
|
153
157
|
readonly customFooter: boolean;
|
|
154
158
|
readonly footerStyle: FooterStyle;
|
|
@@ -179,6 +183,7 @@ export const DEFAULT_SETUP_CONFIG: MyPiSetupConfig = {
|
|
|
179
183
|
maxAgentCalls: DEFAULT_WORKFLOW_MAX_AGENT_CALLS,
|
|
180
184
|
},
|
|
181
185
|
ui: {
|
|
186
|
+
webTheme: "system",
|
|
182
187
|
showHeader: false,
|
|
183
188
|
customFooter: true,
|
|
184
189
|
footerStyle: DEFAULT_FOOTER_STYLE,
|
|
@@ -226,6 +231,9 @@ const isCapabilityDiscoveryMode = (
|
|
|
226
231
|
typeof value === "string" &&
|
|
227
232
|
CAPABILITY_DISCOVERY_MODES.includes(value as CapabilityDiscoveryMode);
|
|
228
233
|
|
|
234
|
+
const isWebTheme = (value: unknown): value is WebTheme =>
|
|
235
|
+
typeof value === "string" && WEB_THEMES.includes(value as WebTheme);
|
|
236
|
+
|
|
229
237
|
export function flattenFooterItems(lines: FooterLines): readonly FooterItem[] {
|
|
230
238
|
const items: FooterItem[] = [];
|
|
231
239
|
const seen = new Set<FooterItem>();
|
|
@@ -485,6 +493,7 @@ export function parseSetupConfig(value: unknown): MyPiSetupConfig {
|
|
|
485
493
|
),
|
|
486
494
|
},
|
|
487
495
|
ui: {
|
|
496
|
+
webTheme: isWebTheme(ui.webTheme) ? ui.webTheme : "system",
|
|
488
497
|
showHeader: typeof ui.showHeader === "boolean" ? ui.showHeader : false,
|
|
489
498
|
customFooter:
|
|
490
499
|
typeof ui.customFooter === "boolean" ? ui.customFooter : true,
|
|
@@ -1016,7 +1025,7 @@ export function formatSetupConfig(config = loadSetupConfig()) {
|
|
|
1016
1025
|
`Capability discovery: ${config.capabilities.discovery}`,
|
|
1017
1026
|
suggestions,
|
|
1018
1027
|
`Workflows: ${config.workflows.concurrency} concurrent agents · ${config.workflows.maxAgentCalls} total calls`,
|
|
1019
|
-
`UI: large header ${config.ui.showHeader ? "on" : "off"} · custom footer ${footer}`,
|
|
1028
|
+
`UI: Web theme ${config.ui.webTheme} · large header ${config.ui.showHeader ? "on" : "off"} · custom footer ${footer}`,
|
|
1020
1029
|
`Subagent results: ${config.ui.subagentResultDisplay === "full" ? "full by default" : "compact status summary (Ctrl+O expands full output)"}`,
|
|
1021
1030
|
`Bash operations: ${config.ui.bashToolDisplay === "full" ? "expanded by default" : "one-line activity summary (Ctrl+O restores native evidence)"}`,
|
|
1022
1031
|
`Write/Edit operations: ${config.ui.fileMutationDisplay === "full" ? "expanded by default" : "one-line activity summary (Ctrl+O restores native evidence)"}`,
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineTool,
|
|
3
|
+
type ToolDefinition,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { type TSchema, Type } from "typebox";
|
|
6
|
+
|
|
7
|
+
export const STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION =
|
|
8
|
+
"When your task is complete, call the `structured_output` tool exactly once as your final action, with fields matching the required schema. Do not write any other text after it.";
|
|
9
|
+
|
|
10
|
+
export const STRUCTURED_OUTPUT_TOOL_DESCRIPTION =
|
|
11
|
+
"Return your final result as structured data matching the required schema. Call this exactly once, as your last action; do not write any other text after it.";
|
|
12
|
+
|
|
13
|
+
export const STRUCTURED_RESULT_LIMITS = Object.freeze({
|
|
14
|
+
schemaDepth: 24,
|
|
15
|
+
schemaNodes: 10_000,
|
|
16
|
+
resultBytes: 2 * 1024 * 1024,
|
|
17
|
+
resultDepth: 24,
|
|
18
|
+
resultNodes: 100_000,
|
|
19
|
+
resultStringBytes: 1024 * 1024,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export interface EncodedStructuredResult {
|
|
23
|
+
readonly value: unknown;
|
|
24
|
+
readonly json: string;
|
|
25
|
+
readonly byteLength: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function safeRecordKey(key: string) {
|
|
29
|
+
return key !== "__proto__" && key !== "constructor" && key !== "prototype";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Preserve the caller's full JSON Schema instead of lossy keyword conversion. */
|
|
33
|
+
export function jsonSchemaToTypebox(schema: unknown): TSchema {
|
|
34
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
35
|
+
throw new Error("structured output schema must be a bounded JSON object");
|
|
36
|
+
}
|
|
37
|
+
const seen = new WeakSet<object>();
|
|
38
|
+
let nodes = 0;
|
|
39
|
+
const validate = (current: unknown, depth: number): boolean => {
|
|
40
|
+
if (
|
|
41
|
+
++nodes > STRUCTURED_RESULT_LIMITS.schemaNodes ||
|
|
42
|
+
depth > STRUCTURED_RESULT_LIMITS.schemaDepth
|
|
43
|
+
) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (
|
|
47
|
+
current === null ||
|
|
48
|
+
typeof current === "string" ||
|
|
49
|
+
typeof current === "boolean"
|
|
50
|
+
) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (typeof current === "number") return Number.isFinite(current);
|
|
54
|
+
if (Array.isArray(current)) {
|
|
55
|
+
return current.every((item) => validate(item, depth + 1));
|
|
56
|
+
}
|
|
57
|
+
if (typeof current !== "object" || seen.has(current)) return false;
|
|
58
|
+
seen.add(current);
|
|
59
|
+
return Object.keys(current).every(
|
|
60
|
+
(key) =>
|
|
61
|
+
safeRecordKey(key) &&
|
|
62
|
+
validate((current as Record<string, unknown>)[key], depth + 1),
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
if (!validate(schema, 0)) {
|
|
66
|
+
throw new Error("structured output schema must be a bounded JSON object");
|
|
67
|
+
}
|
|
68
|
+
return Type.Unsafe(schema);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Encode one complete JSON result or fail before any truncated artifact exists. */
|
|
72
|
+
export function encodeStructuredResult(
|
|
73
|
+
value: unknown,
|
|
74
|
+
): EncodedStructuredResult {
|
|
75
|
+
const seen = new WeakSet<object>();
|
|
76
|
+
let nodes = 0;
|
|
77
|
+
const validate = (current: unknown, depth: number): void => {
|
|
78
|
+
if (++nodes > STRUCTURED_RESULT_LIMITS.resultNodes) {
|
|
79
|
+
throw new Error("structured result exceeds the node limit");
|
|
80
|
+
}
|
|
81
|
+
if (depth > STRUCTURED_RESULT_LIMITS.resultDepth) {
|
|
82
|
+
throw new Error("structured result exceeds the depth limit");
|
|
83
|
+
}
|
|
84
|
+
if (typeof current === "string") {
|
|
85
|
+
if (
|
|
86
|
+
Buffer.byteLength(current, "utf8") >
|
|
87
|
+
STRUCTURED_RESULT_LIMITS.resultStringBytes
|
|
88
|
+
) {
|
|
89
|
+
throw new Error("structured result contains an oversized string");
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (
|
|
94
|
+
current === null ||
|
|
95
|
+
typeof current === "boolean" ||
|
|
96
|
+
(typeof current === "number" && Number.isFinite(current))
|
|
97
|
+
) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (typeof current !== "object" || seen.has(current)) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
"structured result must contain only acyclic JSON values",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
seen.add(current);
|
|
106
|
+
if (Array.isArray(current)) {
|
|
107
|
+
for (const item of current) validate(item, depth + 1);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
for (const [key, item] of Object.entries(current)) {
|
|
111
|
+
if (!safeRecordKey(key)) {
|
|
112
|
+
throw new Error("structured result contains an unsafe object key");
|
|
113
|
+
}
|
|
114
|
+
validate(item, depth + 1);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
validate(value, 0);
|
|
118
|
+
const json = JSON.stringify(value);
|
|
119
|
+
const byteLength = Buffer.byteLength(json, "utf8");
|
|
120
|
+
if (byteLength > STRUCTURED_RESULT_LIMITS.resultBytes) {
|
|
121
|
+
throw new Error("structured result exceeds the total byte limit");
|
|
122
|
+
}
|
|
123
|
+
return { value, json, byteLength };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** One-shot terminating child tool shared by Workflow and Direct Subagent. */
|
|
127
|
+
export function createStructuredOutputTool(
|
|
128
|
+
schema: unknown,
|
|
129
|
+
capture: (value: unknown) => void,
|
|
130
|
+
): ToolDefinition {
|
|
131
|
+
return defineTool({
|
|
132
|
+
name: "structured_output",
|
|
133
|
+
label: "Structured Output",
|
|
134
|
+
description: STRUCTURED_OUTPUT_TOOL_DESCRIPTION,
|
|
135
|
+
parameters: jsonSchemaToTypebox(schema),
|
|
136
|
+
async execute(_toolCallId, params) {
|
|
137
|
+
capture(params);
|
|
138
|
+
return {
|
|
139
|
+
content: [{ type: "text", text: "Recorded structured result." }],
|
|
140
|
+
details: params,
|
|
141
|
+
terminate: true,
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function childToolsWithStructuredOutput(
|
|
148
|
+
tools: readonly string[] | undefined,
|
|
149
|
+
structured: boolean,
|
|
150
|
+
) {
|
|
151
|
+
return tools
|
|
152
|
+
? [...new Set([...tools, ...(structured ? ["structured_output"] : [])])]
|
|
153
|
+
: undefined;
|
|
154
|
+
}
|
|
@@ -57,9 +57,11 @@ import {
|
|
|
57
57
|
} from "../shared/below-editor-navigation.ts";
|
|
58
58
|
import {
|
|
59
59
|
effectiveChildToolAllowlist,
|
|
60
|
+
inheritedChildToolAllowlist,
|
|
60
61
|
resolveStandaloneChildProjectTrust,
|
|
61
62
|
} from "../shared/child-session.ts";
|
|
62
63
|
import { formatContextUtilization } from "../shared/context-utilization.ts";
|
|
64
|
+
import { completionOwnerFor } from "../shared/completion-inbox.ts";
|
|
63
65
|
import {
|
|
64
66
|
registerEditorLayer,
|
|
65
67
|
removeEditorLayer,
|
|
@@ -143,6 +145,7 @@ import { createSubagentResultDelivery } from "./src/result-delivery.ts";
|
|
|
143
145
|
import {
|
|
144
146
|
createSubagentRuntime,
|
|
145
147
|
runTool,
|
|
148
|
+
SubagentToolInterruptedError,
|
|
146
149
|
type SubagentRuntime,
|
|
147
150
|
} from "./src/runtime.ts";
|
|
148
151
|
import { openSubagentPicker, openSubagentTakeover } from "./src/ui/takeover.ts";
|
|
@@ -169,6 +172,7 @@ interface SpawnResultDetails {
|
|
|
169
172
|
readonly harness?: string;
|
|
170
173
|
readonly model?: string;
|
|
171
174
|
readonly agentType?: string;
|
|
175
|
+
readonly structured?: boolean;
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
interface SubagentFinishedData {
|
|
@@ -187,6 +191,8 @@ interface SubagentResultDetails {
|
|
|
187
191
|
readonly elapsed?: string;
|
|
188
192
|
readonly artifactSaveFailed?: boolean;
|
|
189
193
|
readonly fullResultSaved?: boolean;
|
|
194
|
+
readonly structured?: unknown;
|
|
195
|
+
readonly structuredArtifactPath?: string;
|
|
190
196
|
readonly count?: number;
|
|
191
197
|
readonly results?: ReadonlyArray<{
|
|
192
198
|
readonly id: string;
|
|
@@ -197,6 +203,8 @@ interface SubagentResultDetails {
|
|
|
197
203
|
readonly elapsed?: string;
|
|
198
204
|
readonly artifactSaveFailed?: boolean;
|
|
199
205
|
readonly fullResultSaved?: boolean;
|
|
206
|
+
readonly structured?: unknown;
|
|
207
|
+
readonly structuredArtifactPath?: string;
|
|
200
208
|
}>;
|
|
201
209
|
/** Display-only projection for the custom message renderer. */
|
|
202
210
|
readonly displayContent?: string;
|
|
@@ -227,13 +235,17 @@ function describeSubagent(snap: SubagentSnapshot) {
|
|
|
227
235
|
return `${snap.id} [${snap.status}] "${snap.title}" (${details.join(", ")})`;
|
|
228
236
|
}
|
|
229
237
|
|
|
238
|
+
function subagentResultContent(snap: SubagentSnapshot) {
|
|
239
|
+
return snap.structuredResult?.json ?? (snap.finalText || "(no output)");
|
|
240
|
+
}
|
|
241
|
+
|
|
230
242
|
export function truncatedOutput(
|
|
231
243
|
snap: SubagentSnapshot,
|
|
232
244
|
maxBytes = SUBAGENT_OUTPUT_MAX_BYTES,
|
|
233
245
|
writeArtifact: (content: string) => string = (content) =>
|
|
234
246
|
persistResultArtifact(getAgentDir(), content),
|
|
235
247
|
): string {
|
|
236
|
-
const output = snap
|
|
248
|
+
const output = subagentResultContent(snap);
|
|
237
249
|
return projectResult(output, {
|
|
238
250
|
maxBytes: Math.min(maxBytes, DEFAULT_MAX_BYTES),
|
|
239
251
|
maxLines: Math.min(600, DEFAULT_MAX_LINES),
|
|
@@ -245,7 +257,7 @@ function projectSubagentOutput(
|
|
|
245
257
|
snap: SubagentSnapshot,
|
|
246
258
|
maxBytes: number,
|
|
247
259
|
): ResultProjection {
|
|
248
|
-
const output = snap
|
|
260
|
+
const output = subagentResultContent(snap);
|
|
249
261
|
return projectResult(output, {
|
|
250
262
|
maxBytes: Math.min(maxBytes, DEFAULT_MAX_BYTES),
|
|
251
263
|
maxLines: Math.min(600, DEFAULT_MAX_LINES),
|
|
@@ -313,7 +325,7 @@ export function createSubagentResultDispatcher(
|
|
|
313
325
|
);
|
|
314
326
|
const allocation = allocateResultBudgets(
|
|
315
327
|
snaps.map((snap) =>
|
|
316
|
-
Buffer.byteLength(snap
|
|
328
|
+
Buffer.byteLength(subagentResultContent(snap), "utf8"),
|
|
317
329
|
),
|
|
318
330
|
getContextUsage(),
|
|
319
331
|
{
|
|
@@ -370,6 +382,13 @@ export function createSubagentResultDispatcher(
|
|
|
370
382
|
...(projections[0]!.artifactSaveFailed
|
|
371
383
|
? { artifactSaveFailed: true }
|
|
372
384
|
: {}),
|
|
385
|
+
...(snaps[0]!.structuredResult
|
|
386
|
+
? {
|
|
387
|
+
structured: snaps[0]!.structuredResult.value,
|
|
388
|
+
structuredArtifactPath:
|
|
389
|
+
snaps[0]!.structuredResult.artifactPath,
|
|
390
|
+
}
|
|
391
|
+
: {}),
|
|
373
392
|
}
|
|
374
393
|
: {
|
|
375
394
|
count: snaps.length,
|
|
@@ -388,6 +407,12 @@ export function createSubagentResultDispatcher(
|
|
|
388
407
|
...(projections[index]!.artifactSaveFailed
|
|
389
408
|
? { artifactSaveFailed: true }
|
|
390
409
|
: {}),
|
|
410
|
+
...(snap.structuredResult
|
|
411
|
+
? {
|
|
412
|
+
structured: snap.structuredResult.value,
|
|
413
|
+
structuredArtifactPath: snap.structuredResult.artifactPath,
|
|
414
|
+
}
|
|
415
|
+
: {}),
|
|
391
416
|
})),
|
|
392
417
|
};
|
|
393
418
|
pi.appendEntry<SubagentResultEntryData>("subagent-result", {
|
|
@@ -514,6 +539,10 @@ export default function (
|
|
|
514
539
|
);
|
|
515
540
|
const resultDelivery = createSubagentResultDelivery<SubagentSnapshot>({
|
|
516
541
|
isIdle: () => sessionContext?.isIdle() === true,
|
|
542
|
+
owner: () =>
|
|
543
|
+
sessionContext
|
|
544
|
+
? completionOwnerFor(sessionContext.sessionManager)
|
|
545
|
+
: undefined,
|
|
517
546
|
// Every unconsumed fire-and-forget result must reach the parent. The
|
|
518
547
|
// delivery coordinator batches results that settled while it was busy.
|
|
519
548
|
deliver: dispatchResults,
|
|
@@ -859,6 +888,7 @@ export default function (
|
|
|
859
888
|
if (
|
|
860
889
|
planning &&
|
|
861
890
|
agentType &&
|
|
891
|
+
!agentType.planningCompatible &&
|
|
862
892
|
!planModeAllowsDeclaredTools(declaredChildTools)
|
|
863
893
|
) {
|
|
864
894
|
throw new Error(
|
|
@@ -907,7 +937,10 @@ export default function (
|
|
|
907
937
|
const requestedChildTools = planning
|
|
908
938
|
? planModeChildTools(declaredChildTools)
|
|
909
939
|
: declaredChildTools;
|
|
910
|
-
const childTools =
|
|
940
|
+
const childTools = inheritedChildToolAllowlist(
|
|
941
|
+
pi.getActiveTools(),
|
|
942
|
+
requestedChildTools,
|
|
943
|
+
);
|
|
911
944
|
// Read at spawn time so `/openpi-setup` changes affect the next child
|
|
912
945
|
// without reloading this extension. Undefined preserves parent-model
|
|
913
946
|
// inheritance in the backend.
|
|
@@ -931,6 +964,9 @@ export default function (
|
|
|
931
964
|
...(agentType?.body ? { appendSystemPrompt: [agentType.body] } : {}),
|
|
932
965
|
...(childTools ? { tools: childTools } : {}),
|
|
933
966
|
...(agentType ? { agentTypeName: agentType.name } : {}),
|
|
967
|
+
...(params.output_schema !== undefined
|
|
968
|
+
? { outputSchema: params.output_schema }
|
|
969
|
+
: {}),
|
|
934
970
|
...(worktree ? { worktree: { ...worktree, repoCwd: cwd } } : {}),
|
|
935
971
|
parent: {
|
|
936
972
|
parentCwd: ctx.cwd,
|
|
@@ -950,11 +986,22 @@ export default function (
|
|
|
950
986
|
interruptMessage: "Subagent spawn aborted.",
|
|
951
987
|
});
|
|
952
988
|
} catch (error) {
|
|
953
|
-
//
|
|
954
|
-
//
|
|
989
|
+
// Known startup failures can reclaim their empty checkout. Interrupted
|
|
990
|
+
// startup must preserve it while asynchronous acquisition may continue.
|
|
955
991
|
if (worktree) {
|
|
956
992
|
const spawnError =
|
|
957
993
|
error instanceof Error ? error.message : String(error);
|
|
994
|
+
// Cancelling Effect acquisition does not prove an asynchronous
|
|
995
|
+
// factory or extension hook has quiesced. It may still use this cwd.
|
|
996
|
+
if (
|
|
997
|
+
signal?.aborted ||
|
|
998
|
+
error instanceof SubagentToolInterruptedError
|
|
999
|
+
) {
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`${spawnError}; startup quiescence is unknown; checkout preserved at ${worktree.path} (branch ${worktree.branch})`,
|
|
1002
|
+
{ cause: error },
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
958
1005
|
let cleanupWarning: string | undefined;
|
|
959
1006
|
let cleanupError: unknown;
|
|
960
1007
|
try {
|
|
@@ -1000,6 +1047,9 @@ export default function (
|
|
|
1000
1047
|
...(worktree ? { worktreeBranch: worktree.branch } : {}),
|
|
1001
1048
|
...(agentType ? { agentTypeName: agentType.name } : {}),
|
|
1002
1049
|
...(childTools ? { tools: childTools } : {}),
|
|
1050
|
+
...(params.output_schema !== undefined
|
|
1051
|
+
? { structured: true }
|
|
1052
|
+
: {}),
|
|
1003
1053
|
}),
|
|
1004
1054
|
},
|
|
1005
1055
|
],
|
|
@@ -1010,6 +1060,7 @@ export default function (
|
|
|
1010
1060
|
harness,
|
|
1011
1061
|
model: snap.meta.modelLabel,
|
|
1012
1062
|
...(agentType ? { agentType: agentType.name } : {}),
|
|
1063
|
+
...(params.output_schema !== undefined ? { structured: true } : {}),
|
|
1013
1064
|
},
|
|
1014
1065
|
};
|
|
1015
1066
|
},
|
|
@@ -1132,7 +1183,7 @@ export default function (
|
|
|
1132
1183
|
);
|
|
1133
1184
|
const allocation = allocateResultBudgets(
|
|
1134
1185
|
resultEntries.map(({ snap }) =>
|
|
1135
|
-
Buffer.byteLength(snap
|
|
1186
|
+
Buffer.byteLength(subagentResultContent(snap), "utf8"),
|
|
1136
1187
|
),
|
|
1137
1188
|
ctx.getContextUsage(),
|
|
1138
1189
|
{
|
|
@@ -1182,6 +1233,12 @@ export default function (
|
|
|
1182
1233
|
...(artifactSaveFailures.has(id)
|
|
1183
1234
|
? { artifactSaveFailed: true }
|
|
1184
1235
|
: {}),
|
|
1236
|
+
...(snap?.structuredResult
|
|
1237
|
+
? {
|
|
1238
|
+
structured: snap.structuredResult.value,
|
|
1239
|
+
structuredArtifactPath: snap.structuredResult.artifactPath,
|
|
1240
|
+
}
|
|
1241
|
+
: {}),
|
|
1185
1242
|
};
|
|
1186
1243
|
}),
|
|
1187
1244
|
},
|