@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.
Files changed (74) hide show
  1. package/README.md +18 -10
  2. package/SETUP.md +8 -2
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/bin/openpi.js +25 -15
  5. package/extensions/ai-providers/LICENSE.upstream +23 -0
  6. package/extensions/ai-providers/README.md +59 -0
  7. package/extensions/ai-providers/antigravity/credentials.ts +52 -0
  8. package/extensions/ai-providers/antigravity/discovery.ts +130 -0
  9. package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
  10. package/extensions/ai-providers/antigravity/models.ts +84 -0
  11. package/extensions/ai-providers/antigravity/oauth.ts +700 -0
  12. package/extensions/ai-providers/antigravity/provider.ts +1116 -0
  13. package/extensions/ai-providers/antigravity/routing.ts +340 -0
  14. package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
  15. package/extensions/ai-providers/cursor/constants.ts +5 -0
  16. package/extensions/ai-providers/cursor/credentials.ts +14 -0
  17. package/extensions/ai-providers/cursor/discovery.ts +291 -0
  18. package/extensions/ai-providers/cursor/input-images.ts +106 -0
  19. package/extensions/ai-providers/cursor/models.ts +45 -0
  20. package/extensions/ai-providers/cursor/oauth.ts +263 -0
  21. package/extensions/ai-providers/cursor/proto.ts +1064 -0
  22. package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
  23. package/extensions/ai-providers/cursor/provider.ts +1175 -0
  24. package/extensions/ai-providers/cursor/proxy.ts +213 -0
  25. package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
  26. package/extensions/ai-providers/index.ts +86 -0
  27. package/extensions/ai-providers/oauth-adapter.ts +81 -0
  28. package/extensions/ai-providers/usage.ts +10 -0
  29. package/extensions/background-terminals/index.ts +8 -1
  30. package/extensions/background-terminals/src/manager.ts +3 -5
  31. package/extensions/background-terminals/src/result-delivery.ts +43 -23
  32. package/extensions/cron/index.ts +68 -27
  33. package/extensions/cron/schedule.ts +5 -1
  34. package/extensions/model-info/cache-diagnostics.ts +220 -0
  35. package/extensions/model-info/index.ts +45 -1
  36. package/extensions/plan-mode/index.ts +75 -4
  37. package/extensions/setup/index.ts +15 -3
  38. package/extensions/shared/child-session.ts +25 -5
  39. package/extensions/shared/completion-inbox.ts +193 -0
  40. package/extensions/shared/setup-config.ts +10 -1
  41. package/extensions/shared/structured-output.ts +154 -0
  42. package/extensions/subagents/index.ts +44 -4
  43. package/extensions/subagents/src/backends/pi.ts +76 -5
  44. package/extensions/subagents/src/domain.ts +16 -1
  45. package/extensions/subagents/src/manager.ts +5 -0
  46. package/extensions/subagents/src/prompt.ts +17 -3
  47. package/extensions/subagents/src/result-artifact.ts +32 -0
  48. package/extensions/subagents/src/result-delivery.ts +33 -14
  49. package/extensions/ui-customization/footer.ts +16 -5
  50. package/extensions/user-input-fold/index.ts +42 -6
  51. package/extensions/web/index.ts +25 -2
  52. package/extensions/workflows/acceptance.ts +43 -19
  53. package/extensions/workflows/completion-projection.ts +3 -1
  54. package/extensions/workflows/dashboard.ts +8 -0
  55. package/extensions/workflows/index.ts +13 -0
  56. package/extensions/workflows/model.ts +5 -1
  57. package/extensions/workflows/prompt.ts +4 -10
  58. package/extensions/workflows/result-delivery.ts +96 -22
  59. package/extensions/workflows/retention.ts +6 -0
  60. package/extensions/workflows/runner.ts +6 -71
  61. package/package.json +7 -7
  62. package/skills/subagents/REFERENCE.md +3 -2
  63. package/skills/subagents/SKILL.md +1 -0
  64. package/skills/workflows/REFERENCE.md +3 -3
  65. package/skills/workflows/SKILL.md +1 -1
  66. package/web/adapter/pi-adapter.ts +3 -0
  67. package/web/host/pi-coding-agent-entry.ts +162 -0
  68. package/web/host/web-host.ts +330 -50
  69. package/web/protocol/types.ts +5 -0
  70. package/web/runtime/pi-runtime.ts +240 -25
  71. package/web/runtime/types.ts +32 -1
  72. package/web/ui/app.js +343 -41
  73. package/web/ui/index.html +3 -0
  74. 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.finalText || "(no output)";
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.finalText || "(no output)";
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.finalText || "(no output)", "utf8"),
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.finalText || "(no output)", "utf8"),
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
- ...(task.appendSystemPrompt
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
- ...childToolPolicy(task.tools),
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(session, task.tools);
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>;