@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.
Files changed (80) hide show
  1. package/README.md +30 -20
  2. package/SETUP.md +10 -4
  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 +65 -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 +105 -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 +1271 -0
  22. package/extensions/ai-providers/cursor/protobuf.ts +1181 -0
  23. package/extensions/ai-providers/cursor/provider.ts +1431 -0
  24. package/extensions/ai-providers/cursor/proxy.ts +213 -0
  25. package/extensions/ai-providers/cursor/tool-bridge.ts +68 -0
  26. package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
  27. package/extensions/ai-providers/index.ts +86 -0
  28. package/extensions/ai-providers/oauth-adapter.ts +81 -0
  29. package/extensions/ai-providers/usage.ts +10 -0
  30. package/extensions/background-terminals/index.ts +8 -1
  31. package/extensions/background-terminals/src/manager.ts +3 -5
  32. package/extensions/background-terminals/src/result-delivery.ts +43 -23
  33. package/extensions/cron/index.ts +68 -27
  34. package/extensions/cron/schedule.ts +5 -1
  35. package/extensions/model-info/cache-diagnostics.ts +220 -0
  36. package/extensions/model-info/index.ts +45 -1
  37. package/extensions/plan-mode/index.ts +75 -4
  38. package/extensions/setup/index.ts +15 -3
  39. package/extensions/shared/child-session.ts +39 -5
  40. package/extensions/shared/completion-inbox.ts +193 -0
  41. package/extensions/shared/setup-config.ts +10 -1
  42. package/extensions/shared/structured-output.ts +154 -0
  43. package/extensions/subagents/index.ts +64 -7
  44. package/extensions/subagents/src/agent-types.ts +5 -17
  45. package/extensions/subagents/src/backends/pi.ts +130 -48
  46. package/extensions/subagents/src/backends/tool-preview.ts +29 -0
  47. package/extensions/subagents/src/domain.ts +16 -1
  48. package/extensions/subagents/src/manager.ts +7 -71
  49. package/extensions/subagents/src/prompt.ts +19 -5
  50. package/extensions/subagents/src/result-artifact.ts +32 -0
  51. package/extensions/subagents/src/result-delivery.ts +33 -14
  52. package/extensions/subagents/src/runtime.ts +10 -3
  53. package/extensions/ui-customization/footer.ts +16 -5
  54. package/extensions/user-input-fold/index.ts +42 -6
  55. package/extensions/web/index.ts +25 -2
  56. package/extensions/workflows/acceptance.ts +43 -19
  57. package/extensions/workflows/completion-projection.ts +3 -1
  58. package/extensions/workflows/dashboard.ts +147 -21
  59. package/extensions/workflows/index.ts +75 -20
  60. package/extensions/workflows/model.ts +5 -1
  61. package/extensions/workflows/progress-projection.ts +7 -1
  62. package/extensions/workflows/prompt.ts +4 -10
  63. package/extensions/workflows/result-delivery.ts +96 -22
  64. package/extensions/workflows/retention.ts +6 -0
  65. package/extensions/workflows/runner.ts +11 -233
  66. package/extensions/workflows/sandbox.ts +4 -0
  67. package/package.json +7 -7
  68. package/skills/subagents/REFERENCE.md +9 -9
  69. package/skills/subagents/SKILL.md +2 -1
  70. package/skills/workflows/REFERENCE.md +5 -3
  71. package/skills/workflows/SKILL.md +1 -1
  72. package/web/adapter/pi-adapter.ts +3 -0
  73. package/web/host/pi-coding-agent-entry.ts +162 -0
  74. package/web/host/web-host.ts +330 -50
  75. package/web/protocol/types.ts +5 -0
  76. package/web/runtime/pi-runtime.ts +240 -25
  77. package/web/runtime/types.ts +32 -1
  78. package/web/ui/app.js +343 -41
  79. package/web/ui/index.html +3 -0
  80. 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.finalText || "(no output)";
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.finalText || "(no output)";
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.finalText || "(no output)", "utf8"),
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 = effectiveChildToolAllowlist(requestedChildTools);
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
- // The session scope owns reclamation, but it never opened, so this
954
- // worktree would otherwise be orphaned on disk.
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.finalText || "(no output)", "utf8"),
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
  },