@tt-a1i/openpi 0.5.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 +18 -10
- package/SETUP.md +8 -2
- 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 +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 +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 +25 -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 +44 -4
- package/extensions/subagents/src/backends/pi.ts +76 -5
- package/extensions/subagents/src/domain.ts +16 -1
- package/extensions/subagents/src/manager.ts +5 -0
- package/extensions/subagents/src/prompt.ts +17 -3
- package/extensions/subagents/src/result-artifact.ts +32 -0
- package/extensions/subagents/src/result-delivery.ts +33 -14
- 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 +8 -0
- package/extensions/workflows/index.ts +13 -0
- package/extensions/workflows/model.ts +5 -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 +6 -71
- package/package.json +7 -7
- package/skills/subagents/REFERENCE.md +3 -2
- package/skills/subagents/SKILL.md +1 -0
- package/skills/workflows/REFERENCE.md +3 -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
|
@@ -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
|
+
}
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
resolveStandaloneChildProjectTrust,
|
|
61
61
|
} from "../shared/child-session.ts";
|
|
62
62
|
import { formatContextUtilization } from "../shared/context-utilization.ts";
|
|
63
|
+
import { completionOwnerFor } from "../shared/completion-inbox.ts";
|
|
63
64
|
import {
|
|
64
65
|
registerEditorLayer,
|
|
65
66
|
removeEditorLayer,
|
|
@@ -169,6 +170,7 @@ interface SpawnResultDetails {
|
|
|
169
170
|
readonly harness?: string;
|
|
170
171
|
readonly model?: string;
|
|
171
172
|
readonly agentType?: string;
|
|
173
|
+
readonly structured?: boolean;
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
interface SubagentFinishedData {
|
|
@@ -187,6 +189,8 @@ interface SubagentResultDetails {
|
|
|
187
189
|
readonly elapsed?: string;
|
|
188
190
|
readonly artifactSaveFailed?: boolean;
|
|
189
191
|
readonly fullResultSaved?: boolean;
|
|
192
|
+
readonly structured?: unknown;
|
|
193
|
+
readonly structuredArtifactPath?: string;
|
|
190
194
|
readonly count?: number;
|
|
191
195
|
readonly results?: ReadonlyArray<{
|
|
192
196
|
readonly id: string;
|
|
@@ -197,6 +201,8 @@ interface SubagentResultDetails {
|
|
|
197
201
|
readonly elapsed?: string;
|
|
198
202
|
readonly artifactSaveFailed?: boolean;
|
|
199
203
|
readonly fullResultSaved?: boolean;
|
|
204
|
+
readonly structured?: unknown;
|
|
205
|
+
readonly structuredArtifactPath?: string;
|
|
200
206
|
}>;
|
|
201
207
|
/** Display-only projection for the custom message renderer. */
|
|
202
208
|
readonly displayContent?: string;
|
|
@@ -227,13 +233,17 @@ function describeSubagent(snap: SubagentSnapshot) {
|
|
|
227
233
|
return `${snap.id} [${snap.status}] "${snap.title}" (${details.join(", ")})`;
|
|
228
234
|
}
|
|
229
235
|
|
|
236
|
+
function subagentResultContent(snap: SubagentSnapshot) {
|
|
237
|
+
return snap.structuredResult?.json ?? (snap.finalText || "(no output)");
|
|
238
|
+
}
|
|
239
|
+
|
|
230
240
|
export function truncatedOutput(
|
|
231
241
|
snap: SubagentSnapshot,
|
|
232
242
|
maxBytes = SUBAGENT_OUTPUT_MAX_BYTES,
|
|
233
243
|
writeArtifact: (content: string) => string = (content) =>
|
|
234
244
|
persistResultArtifact(getAgentDir(), content),
|
|
235
245
|
): string {
|
|
236
|
-
const output = snap
|
|
246
|
+
const output = subagentResultContent(snap);
|
|
237
247
|
return projectResult(output, {
|
|
238
248
|
maxBytes: Math.min(maxBytes, DEFAULT_MAX_BYTES),
|
|
239
249
|
maxLines: Math.min(600, DEFAULT_MAX_LINES),
|
|
@@ -245,7 +255,7 @@ function projectSubagentOutput(
|
|
|
245
255
|
snap: SubagentSnapshot,
|
|
246
256
|
maxBytes: number,
|
|
247
257
|
): ResultProjection {
|
|
248
|
-
const output = snap
|
|
258
|
+
const output = subagentResultContent(snap);
|
|
249
259
|
return projectResult(output, {
|
|
250
260
|
maxBytes: Math.min(maxBytes, DEFAULT_MAX_BYTES),
|
|
251
261
|
maxLines: Math.min(600, DEFAULT_MAX_LINES),
|
|
@@ -313,7 +323,7 @@ export function createSubagentResultDispatcher(
|
|
|
313
323
|
);
|
|
314
324
|
const allocation = allocateResultBudgets(
|
|
315
325
|
snaps.map((snap) =>
|
|
316
|
-
Buffer.byteLength(snap
|
|
326
|
+
Buffer.byteLength(subagentResultContent(snap), "utf8"),
|
|
317
327
|
),
|
|
318
328
|
getContextUsage(),
|
|
319
329
|
{
|
|
@@ -370,6 +380,13 @@ export function createSubagentResultDispatcher(
|
|
|
370
380
|
...(projections[0]!.artifactSaveFailed
|
|
371
381
|
? { artifactSaveFailed: true }
|
|
372
382
|
: {}),
|
|
383
|
+
...(snaps[0]!.structuredResult
|
|
384
|
+
? {
|
|
385
|
+
structured: snaps[0]!.structuredResult.value,
|
|
386
|
+
structuredArtifactPath:
|
|
387
|
+
snaps[0]!.structuredResult.artifactPath,
|
|
388
|
+
}
|
|
389
|
+
: {}),
|
|
373
390
|
}
|
|
374
391
|
: {
|
|
375
392
|
count: snaps.length,
|
|
@@ -388,6 +405,12 @@ export function createSubagentResultDispatcher(
|
|
|
388
405
|
...(projections[index]!.artifactSaveFailed
|
|
389
406
|
? { artifactSaveFailed: true }
|
|
390
407
|
: {}),
|
|
408
|
+
...(snap.structuredResult
|
|
409
|
+
? {
|
|
410
|
+
structured: snap.structuredResult.value,
|
|
411
|
+
structuredArtifactPath: snap.structuredResult.artifactPath,
|
|
412
|
+
}
|
|
413
|
+
: {}),
|
|
391
414
|
})),
|
|
392
415
|
};
|
|
393
416
|
pi.appendEntry<SubagentResultEntryData>("subagent-result", {
|
|
@@ -514,6 +537,10 @@ export default function (
|
|
|
514
537
|
);
|
|
515
538
|
const resultDelivery = createSubagentResultDelivery<SubagentSnapshot>({
|
|
516
539
|
isIdle: () => sessionContext?.isIdle() === true,
|
|
540
|
+
owner: () =>
|
|
541
|
+
sessionContext
|
|
542
|
+
? completionOwnerFor(sessionContext.sessionManager)
|
|
543
|
+
: undefined,
|
|
517
544
|
// Every unconsumed fire-and-forget result must reach the parent. The
|
|
518
545
|
// delivery coordinator batches results that settled while it was busy.
|
|
519
546
|
deliver: dispatchResults,
|
|
@@ -931,6 +958,9 @@ export default function (
|
|
|
931
958
|
...(agentType?.body ? { appendSystemPrompt: [agentType.body] } : {}),
|
|
932
959
|
...(childTools ? { tools: childTools } : {}),
|
|
933
960
|
...(agentType ? { agentTypeName: agentType.name } : {}),
|
|
961
|
+
...(params.output_schema !== undefined
|
|
962
|
+
? { outputSchema: params.output_schema }
|
|
963
|
+
: {}),
|
|
934
964
|
...(worktree ? { worktree: { ...worktree, repoCwd: cwd } } : {}),
|
|
935
965
|
parent: {
|
|
936
966
|
parentCwd: ctx.cwd,
|
|
@@ -1000,6 +1030,9 @@ export default function (
|
|
|
1000
1030
|
...(worktree ? { worktreeBranch: worktree.branch } : {}),
|
|
1001
1031
|
...(agentType ? { agentTypeName: agentType.name } : {}),
|
|
1002
1032
|
...(childTools ? { tools: childTools } : {}),
|
|
1033
|
+
...(params.output_schema !== undefined
|
|
1034
|
+
? { structured: true }
|
|
1035
|
+
: {}),
|
|
1003
1036
|
}),
|
|
1004
1037
|
},
|
|
1005
1038
|
],
|
|
@@ -1010,6 +1043,7 @@ export default function (
|
|
|
1010
1043
|
harness,
|
|
1011
1044
|
model: snap.meta.modelLabel,
|
|
1012
1045
|
...(agentType ? { agentType: agentType.name } : {}),
|
|
1046
|
+
...(params.output_schema !== undefined ? { structured: true } : {}),
|
|
1013
1047
|
},
|
|
1014
1048
|
};
|
|
1015
1049
|
},
|
|
@@ -1132,7 +1166,7 @@ export default function (
|
|
|
1132
1166
|
);
|
|
1133
1167
|
const allocation = allocateResultBudgets(
|
|
1134
1168
|
resultEntries.map(({ snap }) =>
|
|
1135
|
-
Buffer.byteLength(snap
|
|
1169
|
+
Buffer.byteLength(subagentResultContent(snap), "utf8"),
|
|
1136
1170
|
),
|
|
1137
1171
|
ctx.getContextUsage(),
|
|
1138
1172
|
{
|
|
@@ -1182,6 +1216,12 @@ export default function (
|
|
|
1182
1216
|
...(artifactSaveFailures.has(id)
|
|
1183
1217
|
? { artifactSaveFailed: true }
|
|
1184
1218
|
: {}),
|
|
1219
|
+
...(snap?.structuredResult
|
|
1220
|
+
? {
|
|
1221
|
+
structured: snap.structuredResult.value,
|
|
1222
|
+
structuredArtifactPath: snap.structuredResult.artifactPath,
|
|
1223
|
+
}
|
|
1224
|
+
: {}),
|
|
1185
1225
|
};
|
|
1186
1226
|
}),
|
|
1187
1227
|
},
|
|
@@ -19,6 +19,7 @@ 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";
|
|
@@ -49,6 +50,14 @@ import {
|
|
|
49
50
|
reclaimWorktree,
|
|
50
51
|
} from "../../../shared/worktree.ts";
|
|
51
52
|
import { AgentToolRenderLedger } from "../../../shared/agent-tool-renderer.ts";
|
|
53
|
+
import {
|
|
54
|
+
childToolsWithStructuredOutput,
|
|
55
|
+
createStructuredOutputTool,
|
|
56
|
+
encodeStructuredResult,
|
|
57
|
+
type EncodedStructuredResult,
|
|
58
|
+
STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION,
|
|
59
|
+
} from "../../../shared/structured-output.ts";
|
|
60
|
+
import { persistStructuredResultArtifact } from "../result-artifact.ts";
|
|
52
61
|
|
|
53
62
|
const DIRECT_WORKTREE_CLEANUP_TIMEOUT_MS = 4_000;
|
|
54
63
|
const PARTIAL_TEXT_MAX_LENGTH = 128 * 1_024;
|
|
@@ -198,14 +207,26 @@ const makePiSession = (
|
|
|
198
207
|
const thinkingLevel = (task.reasoningEffort ??
|
|
199
208
|
task.parent.inheritedThinkingLevel) as ThinkingLevel | undefined;
|
|
200
209
|
|
|
210
|
+
let capturedStructured: EncodedStructuredResult | undefined;
|
|
211
|
+
const structuredOutputTool =
|
|
212
|
+
task.outputSchema === undefined
|
|
213
|
+
? undefined
|
|
214
|
+
: createStructuredOutputTool(task.outputSchema, (value) => {
|
|
215
|
+
capturedStructured = encodeStructuredResult(value);
|
|
216
|
+
});
|
|
217
|
+
|
|
201
218
|
const session = yield* Effect.tryPromise({
|
|
202
219
|
try: async () => {
|
|
220
|
+
const appendSystemPrompt = [
|
|
221
|
+
...(task.appendSystemPrompt ?? []),
|
|
222
|
+
...(structuredOutputTool
|
|
223
|
+
? [STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION]
|
|
224
|
+
: []),
|
|
225
|
+
];
|
|
203
226
|
const { loader, settingsManager } = await createChildResources({
|
|
204
227
|
cwd: task.cwd,
|
|
205
228
|
projectTrusted: task.parent.projectTrusted,
|
|
206
|
-
...(
|
|
207
|
-
? { appendSystemPrompt: [...task.appendSystemPrompt] }
|
|
208
|
-
: {}),
|
|
229
|
+
...(appendSystemPrompt.length > 0 ? { appendSystemPrompt } : {}),
|
|
209
230
|
});
|
|
210
231
|
const { session } = await (
|
|
211
232
|
options.sessionFactory ?? createAgentSession
|
|
@@ -216,13 +237,27 @@ const makePiSession = (
|
|
|
216
237
|
resourceLoader: loader,
|
|
217
238
|
model,
|
|
218
239
|
thinkingLevel,
|
|
219
|
-
...
|
|
240
|
+
...(structuredOutputTool
|
|
241
|
+
? { customTools: [structuredOutputTool] }
|
|
242
|
+
: {}),
|
|
243
|
+
...childToolPolicy(
|
|
244
|
+
childToolsWithStructuredOutput(
|
|
245
|
+
task.tools,
|
|
246
|
+
structuredOutputTool !== undefined,
|
|
247
|
+
),
|
|
248
|
+
),
|
|
220
249
|
});
|
|
221
250
|
// Start child extension session hooks/resources in headless mode.
|
|
222
251
|
// A rejection here would otherwise leak the freshly created session:
|
|
223
252
|
// the scope finalizer that owns cleanup is only registered later.
|
|
224
253
|
try {
|
|
225
|
-
await bindChildSessionExtensions(
|
|
254
|
+
await bindChildSessionExtensions(
|
|
255
|
+
session,
|
|
256
|
+
childToolsWithStructuredOutput(
|
|
257
|
+
task.tools,
|
|
258
|
+
structuredOutputTool !== undefined,
|
|
259
|
+
),
|
|
260
|
+
);
|
|
226
261
|
} catch (error) {
|
|
227
262
|
await shutdownAndDisposeChildSession(session, {
|
|
228
263
|
timeoutMs: options.shutdownTimeoutMs,
|
|
@@ -338,11 +373,46 @@ const makePiSession = (
|
|
|
338
373
|
});
|
|
339
374
|
return;
|
|
340
375
|
}
|
|
376
|
+
if (task.outputSchema !== undefined && capturedStructured === undefined) {
|
|
377
|
+
emit({
|
|
378
|
+
_tag: "RunSettled",
|
|
379
|
+
outcome: {
|
|
380
|
+
_tag: "Failed",
|
|
381
|
+
errorText:
|
|
382
|
+
"Agent finished without calling structured_output; no structured result matching output_schema was produced.",
|
|
383
|
+
partialText,
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
let structuredResult;
|
|
389
|
+
if (capturedStructured) {
|
|
390
|
+
try {
|
|
391
|
+
structuredResult = {
|
|
392
|
+
...capturedStructured,
|
|
393
|
+
artifactPath: persistStructuredResultArtifact(
|
|
394
|
+
getAgentDir(),
|
|
395
|
+
capturedStructured.json,
|
|
396
|
+
),
|
|
397
|
+
};
|
|
398
|
+
} catch (error) {
|
|
399
|
+
emit({
|
|
400
|
+
_tag: "RunSettled",
|
|
401
|
+
outcome: {
|
|
402
|
+
_tag: "Failed",
|
|
403
|
+
errorText: `Structured result artifact could not be persisted: ${boundedError(error)}`,
|
|
404
|
+
partialText,
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
341
410
|
emit({
|
|
342
411
|
_tag: "RunSettled",
|
|
343
412
|
outcome: {
|
|
344
413
|
_tag: "Completed",
|
|
345
414
|
finalText: prompt.finalText,
|
|
415
|
+
...(structuredResult ? { structuredResult } : {}),
|
|
346
416
|
},
|
|
347
417
|
});
|
|
348
418
|
};
|
|
@@ -597,6 +667,7 @@ const makePiSession = (
|
|
|
597
667
|
promise: Promise.resolve(),
|
|
598
668
|
};
|
|
599
669
|
state.activePrompt = activePrompt;
|
|
670
|
+
capturedStructured = undefined;
|
|
600
671
|
state.settled = false;
|
|
601
672
|
emit({ _tag: "RunStarted" });
|
|
602
673
|
let prompt: Promise<void>;
|