pi-subagents 0.55.0 → 0.56.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 (32) hide show
  1. package/CHANGELOG.md +28 -3
  2. package/docs/models.md +6 -0
  3. package/package.json +1 -1
  4. package/src/agents/agent-serializer.ts +2 -0
  5. package/src/agents/agents.ts +23 -1
  6. package/src/api/preflight.ts +20 -1
  7. package/src/extension/schemas.ts +5 -0
  8. package/src/runs/background/async-execution.ts +48 -18
  9. package/src/runs/background/async-resume.ts +3 -0
  10. package/src/runs/background/subagent-runner.ts +83 -11
  11. package/src/runs/foreground/execution.ts +35 -6
  12. package/src/runs/foreground/foreground-history.ts +3 -2
  13. package/src/runs/foreground/subagent-executor.ts +59 -8
  14. package/src/runs/foreground/workflow-detach-reconcile.ts +15 -6
  15. package/src/runs/shared/acceptance.ts +10 -5
  16. package/src/runs/shared/agent-contract.ts +1 -1
  17. package/src/runs/shared/completion-guard.ts +3 -2
  18. package/src/runs/shared/dynamic-fanout.ts +1 -1
  19. package/src/runs/shared/extension-bindings.ts +78 -0
  20. package/src/runs/shared/external-cli-runner.ts +2 -0
  21. package/src/runs/shared/fast-mode-extension.ts +10 -0
  22. package/src/runs/shared/model-exclusions.ts +2 -1
  23. package/src/runs/shared/model-fallback.ts +2 -7
  24. package/src/runs/shared/mutation-evidence.ts +145 -0
  25. package/src/runs/shared/parallel-utils.ts +3 -0
  26. package/src/runs/shared/pi-args.ts +47 -0
  27. package/src/runs/shared/structured-output.ts +18 -4
  28. package/src/runs/shared/subagent-prompt-runtime.ts +9 -5
  29. package/src/shared/launch-contract.ts +6 -0
  30. package/src/shared/settings.ts +6 -1
  31. package/src/shared/types.ts +48 -0
  32. package/src/workflows/scripted-workflow.ts +50 -3
@@ -0,0 +1,145 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import * as fs from "node:fs";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ import type { ArtifactPaths, TimeoutRecoverySummary, TrackedMutationEvidence, TrackedMutationFingerprint, TrackedMutationSnapshot } from "../../shared/types.ts";
7
+
8
+ const MAX_TRACKED_PATHS = 500;
9
+ const MAX_HASH_BYTES = 1024 * 1024;
10
+ const MAX_TIMEOUT_FILES = 20;
11
+
12
+ function gitOutput(cwd: string, args: string[], maxBuffer = MAX_HASH_BYTES): string {
13
+ return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer });
14
+ }
15
+
16
+ function splitNul(output: string): string[] {
17
+ return output.split("\0").filter((part) => part.length > 0);
18
+ }
19
+
20
+ function hashLargeDiff(cwd: string, relativePath: string): string {
21
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tracked-diff-"));
22
+ const diffPath = path.join(tempDir, "diff.patch");
23
+ try {
24
+ execFileSync("git", ["diff", "--no-ext-diff", "--binary", `--output=${diffPath}`, "HEAD", "--", relativePath], { cwd, stdio: "ignore" });
25
+ const hash = createHash("sha256");
26
+ const buffer = Buffer.allocUnsafe(64 * 1024);
27
+ const fd = fs.openSync(diffPath, "r");
28
+ try {
29
+ for (;;) {
30
+ const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null);
31
+ if (bytesRead === 0) break;
32
+ hash.update(buffer.subarray(0, bytesRead));
33
+ }
34
+ } finally {
35
+ fs.closeSync(fd);
36
+ }
37
+ return hash.digest("hex");
38
+ } finally {
39
+ fs.rmSync(tempDir, { recursive: true, force: true });
40
+ }
41
+ }
42
+
43
+ function listChangedTrackedFiles(cwd: string): { paths: string[]; truncated: boolean } {
44
+ const paths = splitNul(gitOutput(cwd, ["diff", "--no-ext-diff", "--name-only", "-z", "HEAD", "--"], MAX_HASH_BYTES));
45
+ return { paths: paths.slice(0, MAX_TRACKED_PATHS), truncated: paths.length > MAX_TRACKED_PATHS };
46
+ }
47
+
48
+ function fingerprintPath(cwd: string, relativePath: string): TrackedMutationFingerprint {
49
+ try {
50
+ const diff = gitOutput(cwd, ["diff", "--no-ext-diff", "--binary", "HEAD", "--", relativePath], MAX_HASH_BYTES);
51
+ return { kind: "diff", digest: createHash("sha256").update(diff).digest("hex") };
52
+ } catch {
53
+ return { kind: "diff", digest: hashLargeDiff(cwd, relativePath) };
54
+ }
55
+ }
56
+
57
+ function sameFingerprint(left: TrackedMutationFingerprint | undefined, right: TrackedMutationFingerprint): boolean {
58
+ if (!left || left.kind !== "diff" || right.kind !== "diff") return false;
59
+ return left.digest === right.digest;
60
+ }
61
+
62
+ export function snapshotTrackedMutations(cwd: string): TrackedMutationSnapshot {
63
+ try {
64
+ const changed = listChangedTrackedFiles(cwd);
65
+ const fingerprints: Record<string, TrackedMutationFingerprint> = {};
66
+ for (const file of changed.paths) fingerprints[file] = fingerprintPath(cwd, file);
67
+ return { source: "tracked-files", trackedOnly: true, cwd, dirtyFiles: changed.paths, fingerprints, truncated: changed.truncated };
68
+ } catch (error) {
69
+ return { source: "tracked-files", trackedOnly: true, cwd, dirtyFiles: [], fingerprints: {}, unavailable: error instanceof Error ? error.message : String(error) };
70
+ }
71
+ }
72
+
73
+ export function collectTrackedMutationEvidence(snapshot: TrackedMutationSnapshot, cwd = snapshot.cwd): TrackedMutationEvidence {
74
+ if (snapshot.unavailable) {
75
+ return { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: false, unavailable: snapshot.unavailable };
76
+ }
77
+ try {
78
+ const current = listChangedTrackedFiles(cwd);
79
+ const startDirty = new Set(snapshot.dirtyFiles);
80
+ const candidates = new Set([...snapshot.dirtyFiles, ...current.paths]);
81
+ const changedFiles: string[] = [];
82
+ for (const file of candidates) {
83
+ if (!startDirty.has(file)) {
84
+ if (!snapshot.truncated) changedFiles.push(file);
85
+ continue;
86
+ }
87
+ if (!sameFingerprint(snapshot.fingerprints[file], fingerprintPath(cwd, file))) changedFiles.push(file);
88
+ }
89
+ changedFiles.sort();
90
+ return {
91
+ source: "tracked-files",
92
+ trackedOnly: true,
93
+ changedFiles,
94
+ attemptedMutation: changedFiles.length > 0,
95
+ truncated: snapshot.truncated || current.truncated || undefined,
96
+ };
97
+ } catch (error) {
98
+ return { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: false, unavailable: error instanceof Error ? error.message : String(error) };
99
+ }
100
+ }
101
+
102
+ function formatPathList(paths: string[]): string {
103
+ if (paths.length === 0) return "none";
104
+ const shown = paths.slice(0, MAX_TIMEOUT_FILES).join(", ");
105
+ return paths.length > MAX_TIMEOUT_FILES ? `${shown}, ... (${paths.length - MAX_TIMEOUT_FILES} more)` : shown;
106
+ }
107
+
108
+ export function buildTimeoutRecoverySummary(input: {
109
+ termination: "timed-out" | "stopped";
110
+ evidence: TrackedMutationEvidence;
111
+ currentTool?: string;
112
+ currentToolArgs?: string;
113
+ currentPath?: string;
114
+ sessionFile?: string;
115
+ transcriptPath?: string;
116
+ artifactPaths?: ArtifactPaths;
117
+ }): TimeoutRecoverySummary {
118
+ const warning = "Inspect partial changes before retrying or resuming the child.";
119
+ const changedFiles = input.evidence.changedFiles.slice(0, MAX_TIMEOUT_FILES);
120
+ const lines = [
121
+ "Recovery summary:",
122
+ `- termination: ${input.termination}`,
123
+ `- changed tracked files: ${input.evidence.unavailable ? `unavailable (${input.evidence.unavailable})` : formatPathList(input.evidence.changedFiles)}`,
124
+ ];
125
+ if (input.currentTool) lines.push(`- active tool: ${input.currentTool}${input.currentToolArgs ? ` — ${input.currentToolArgs}` : ""}`);
126
+ if (input.currentPath) lines.push(`- active path: ${input.currentPath}`);
127
+ if (input.sessionFile) lines.push(`- session file: ${input.sessionFile}`);
128
+ if (input.transcriptPath) lines.push(`- transcript: ${input.transcriptPath}`);
129
+ if (input.artifactPaths?.outputPath) lines.push(`- output artifact: ${input.artifactPaths.outputPath}`);
130
+ if (input.artifactPaths?.metadataPath) lines.push(`- metadata artifact: ${input.artifactPaths.metadataPath}`);
131
+ lines.push(`Warning: ${warning}`);
132
+ return {
133
+ termination: input.termination,
134
+ changedFiles,
135
+ truncated: input.evidence.changedFiles.length > changedFiles.length || input.evidence.truncated || undefined,
136
+ currentTool: input.currentTool,
137
+ currentToolArgs: input.currentToolArgs,
138
+ currentPath: input.currentPath,
139
+ sessionFile: input.sessionFile,
140
+ transcriptPath: input.transcriptPath,
141
+ artifactPaths: input.artifactPaths,
142
+ warning,
143
+ message: lines.join("\n"),
144
+ };
145
+ }
@@ -29,6 +29,7 @@ export interface RunnerSubagentStep {
29
29
  structured?: boolean;
30
30
  cwd?: string;
31
31
  model?: string;
32
+ fast?: boolean;
32
33
  thinking?: string;
33
34
  thinkingCeiling?: import("../../shared/model-info.ts").ThinkingLevel;
34
35
  modelCandidates?: string[];
@@ -59,12 +60,14 @@ export interface RunnerSubagentStep {
59
60
  schema: import("../../shared/types.ts").JsonSchemaObject;
60
61
  schemaPath: string;
61
62
  outputPath: string;
63
+ acceptanceReportPath?: string;
62
64
  };
63
65
  structuredOutputSchema?: import("../../shared/types.ts").JsonSchemaObject;
64
66
  agentContract?: import("../../shared/types.ts").AgentContract;
65
67
  definitionDigest?: string;
66
68
  launchBindingTask?: string;
67
69
  launchContractDigest?: string;
70
+ extensionBindings?: import("./extension-bindings.ts").ExtensionBindings;
68
71
  launchResolvedExtensions?: import("../../shared/types.ts").LaunchResolvedChildExtensionsV1;
69
72
  runtimeAcknowledgedExtensions?: import("../../shared/types.ts").RuntimeAcknowledgedChildExtensionsV1;
70
73
  effectiveAcceptance?: import("../../shared/types.ts").ResolvedAcceptanceConfig;
@@ -13,8 +13,10 @@ import {
13
13
  type ResolvedMcpDirectToolSelection,
14
14
  } from "./mcp-direct-tool-allowlist.ts";
15
15
  import { resolvePiPackageRoot } from "./pi-spawn.ts";
16
+ import { encodeExtensionBindings, PI_SUBAGENT_EXTENSION_BINDINGS_ENV, type ExtensionBindings } from "./extension-bindings.ts";
16
17
  import { RUNTIME_EXTENSION_ACK_PATH_ENV } from "./runtime-acknowledged-extensions.ts";
17
18
  import {
19
+ STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV,
18
20
  STRUCTURED_OUTPUT_CAPTURE_ENV,
19
21
  STRUCTURED_OUTPUT_SCHEMA_ENV,
20
22
  } from "./structured-output.ts";
@@ -104,6 +106,14 @@ const FANOUT_CHILD_EXTENSION_PATH = path.join(
104
106
  "extension",
105
107
  "fanout-child.ts",
106
108
  );
109
+ const FAST_MODE_EXTENSION_PATH = path.join(
110
+ path.dirname(fileURLToPath(import.meta.url)),
111
+ "fast-mode-extension.ts",
112
+ );
113
+ const FAST_MODE_ALLOWED_MODELS = new Set([
114
+ "openai-codex/gpt-5.6-luna",
115
+ "openai-codex/gpt-5.6-sol",
116
+ ]);
107
117
  export const SUBAGENT_CHILD_ENV = "PI_SUBAGENT_CHILD";
108
118
  export const SUBAGENT_ORCHESTRATOR_TARGET_ENV =
109
119
  "PI_SUBAGENT_ORCHESTRATOR_TARGET";
@@ -173,7 +183,10 @@ export interface BuildPiArgsInput {
173
183
  schema: JsonSchemaObject;
174
184
  schemaPath: string;
175
185
  outputPath: string;
186
+ acceptanceReportPath?: string;
176
187
  };
188
+ fast?: boolean;
189
+ modelCandidates?: readonly string[];
177
190
  toolBudget?: ResolvedToolBudget;
178
191
  allowZeroToolBudget?: boolean;
179
192
  permissionRules?: PermissionRules;
@@ -188,6 +201,7 @@ export interface BuildPiArgsInput {
188
201
  waitToolEnabled?: boolean;
189
202
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
190
203
  thinkingCeiling?: import("../../shared/model-info.ts").ThinkingLevel;
204
+ extensionBindings?: ExtensionBindings;
191
205
  }
192
206
 
193
207
  export interface BuildPiArgsResult {
@@ -237,6 +251,28 @@ export function applyThinkingSuffix(
237
251
  return `${model}:${thinking}`;
238
252
  }
239
253
 
254
+ function stripThinkingSuffix(model: string): string {
255
+ const colonIdx = model.lastIndexOf(":");
256
+ if (colonIdx === -1) return model;
257
+ return THINKING_LEVELS.some((level) => level === model.substring(colonIdx + 1))
258
+ ? model.slice(0, colonIdx)
259
+ : model;
260
+ }
261
+
262
+ function resolveFastModeExtension(input: Pick<ResolvePiLaunchToolPlanInput, "fast" | "model" | "modelCandidates" | "agentName">): string[] {
263
+ if (!input.fast) return [];
264
+ const candidates = (input.modelCandidates?.length ? input.modelCandidates : input.model ? [input.model] : [])
265
+ .map(stripThinkingSuffix);
266
+ if (candidates.length === 0) {
267
+ throw new Error(`fast mode requires an explicit supported native OpenAI-Codex model${input.agentName ? ` for agent '${input.agentName}'` : ""}.`);
268
+ }
269
+ const unsupported = candidates.filter((model) => !FAST_MODE_ALLOWED_MODELS.has(model));
270
+ if (unsupported.length > 0) {
271
+ throw new Error(`fast mode supports only ${[...FAST_MODE_ALLOWED_MODELS].join(", ")}; unsupported model${unsupported.length === 1 ? "" : "s"}: ${unsupported.join(", ")}.`);
272
+ }
273
+ return [FAST_MODE_EXTENSION_PATH];
274
+ }
275
+
240
276
  export interface ResolvePiLaunchToolPlanInput {
241
277
  tools?: string[];
242
278
  extensions?: string[];
@@ -251,6 +287,9 @@ export interface ResolvePiLaunchToolPlanInput {
251
287
  schemaPath: string;
252
288
  outputPath: string;
253
289
  };
290
+ fast?: boolean;
291
+ model?: string;
292
+ modelCandidates?: readonly string[];
254
293
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
255
294
  inheritedCapabilityCeiling?: ResolvedSubagentCapabilityCeiling;
256
295
  agentName?: string;
@@ -464,8 +503,11 @@ export function resolvePiLaunchToolPlan(
464
503
  : hasPermissionRules(input.permissionRules)
465
504
  ? resolvePermissionSystemExtension()
466
505
  : undefined;
506
+ if (input.fast && capabilityCeiling?.denyExtensions) throw new Error("fast mode requires a child runtime extension, but this launch denies extensions.");
507
+ const fastModeExtensions = resolveFastModeExtension({ fast: input.fast, model: input.model, modelCandidates: input.modelCandidates, agentName: input.agentName });
467
508
  const runtimeExtensions = [
468
509
  PROMPT_RUNTIME_EXTENSION_PATH,
510
+ ...fastModeExtensions,
469
511
  ...(fanoutAuthorized ? [FANOUT_CHILD_EXTENSION_PATH] : []),
470
512
  ...(permSystemExt ? [permSystemExt] : []),
471
513
  ];
@@ -605,6 +647,9 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
605
647
  cwd: input.cwd,
606
648
  requireReadTool: input.requireReadTool,
607
649
  structuredOutput: input.structuredOutput,
650
+ fast: input.fast,
651
+ model: modelArg,
652
+ modelCandidates: input.modelCandidates,
608
653
  capabilityCeiling: input.capabilityCeiling,
609
654
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(
610
655
  process.env[SUBAGENT_CAPABILITY_CEILING_ENV],
@@ -668,6 +713,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
668
713
  }
669
714
 
670
715
  const env: Record<string, string | undefined> = {};
716
+ env[PI_SUBAGENT_EXTENSION_BINDINGS_ENV] = encodeExtensionBindings(input.extensionBindings);
671
717
  const piPackageRoot =
672
718
  process.env[PI_CODING_AGENT_PACKAGE_ROOT_ENV] ?? resolvePiPackageRoot();
673
719
  if (piPackageRoot) env[PI_CODING_AGENT_PACKAGE_ROOT_ENV] = piPackageRoot;
@@ -838,6 +884,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
838
884
  if (input.structuredOutput) {
839
885
  env[STRUCTURED_OUTPUT_CAPTURE_ENV] = input.structuredOutput.outputPath;
840
886
  env[STRUCTURED_OUTPUT_SCHEMA_ENV] = input.structuredOutput.schemaPath;
887
+ if (input.structuredOutput.acceptanceReportPath) env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV] = input.structuredOutput.acceptanceReportPath;
841
888
  }
842
889
  if (input.steerInboxDir) {
843
890
  env[SUBAGENT_STEER_INBOX_ENV] = input.steerInboxDir;
@@ -8,12 +8,14 @@ import type { JsonSchemaObject } from "../../shared/types.ts";
8
8
 
9
9
  export const STRUCTURED_OUTPUT_SCHEMA_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA";
10
10
  export const STRUCTURED_OUTPUT_CAPTURE_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE";
11
+ export const STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE";
11
12
  export const MISSING_STRUCTURED_OUTPUT_CALL_ERROR = "Missing structured_output call; this step has outputSchema and must finish by calling structured_output.";
12
13
 
13
14
  export interface StructuredOutputRuntime {
14
15
  schema: JsonSchemaObject;
15
16
  schemaPath: string;
16
17
  outputPath: string;
18
+ acceptanceReportPath?: string;
17
19
  }
18
20
 
19
21
  const SCHEMA_MAP_KEYWORDS = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"] as const;
@@ -59,10 +61,13 @@ function rewriteLocalJsonPointerRefs(schema: unknown, pointerPrefix: string, inh
59
61
  return rewritten;
60
62
  }
61
63
 
62
- export function createStructuredOutputToolParameters(schema: JsonSchemaObject): JsonSchemaObject {
64
+ export function createStructuredOutputToolParameters(schema: JsonSchemaObject, options: { acceptanceReport?: boolean } = {}): JsonSchemaObject {
63
65
  return {
64
66
  type: "object",
65
- properties: { value: rewriteLocalJsonPointerRefs(schema, "#/properties/value") },
67
+ properties: {
68
+ value: rewriteLocalJsonPointerRefs(schema, "#/properties/value"),
69
+ ...(options.acceptanceReport ? { acceptanceReport: { type: "object" } } : {}),
70
+ },
66
71
  required: ["value"],
67
72
  additionalProperties: false,
68
73
  };
@@ -124,7 +129,7 @@ export function assertJsonSchemaObject(schema: unknown, label = "outputSchema"):
124
129
  }
125
130
  }
126
131
 
127
- export function createStructuredOutputRuntime(schema: JsonSchemaObject, baseDir?: string): StructuredOutputRuntime {
132
+ export function createStructuredOutputRuntime(schema: JsonSchemaObject, baseDir?: string, options: { captureAcceptanceReport?: boolean } = {}): StructuredOutputRuntime {
128
133
  assertJsonSchemaObject(schema);
129
134
  const rootDir = baseDir ?? os.tmpdir();
130
135
  fs.mkdirSync(rootDir, { recursive: true });
@@ -132,7 +137,7 @@ export function createStructuredOutputRuntime(schema: JsonSchemaObject, baseDir?
132
137
  const schemaPath = path.join(dir, "schema.json");
133
138
  const outputPath = path.join(dir, "output.json");
134
139
  fs.writeFileSync(schemaPath, JSON.stringify(schema), { mode: 0o600 });
135
- return { schema, schemaPath, outputPath };
140
+ return { schema, schemaPath, outputPath, ...(options.captureAcceptanceReport ? { acceptanceReportPath: path.join(dir, "acceptance-report.json") } : {}) };
136
141
  }
137
142
 
138
143
  export async function validateStructuredOutputValue(schema: JsonSchemaObject, value: unknown): Promise<{ status: "valid" } | { status: "invalid"; message: string }> {
@@ -172,6 +177,15 @@ export async function readStructuredOutput(runtime: StructuredOutputRuntime): Pr
172
177
  return { value };
173
178
  }
174
179
 
180
+ export function readStructuredOutputAcceptanceReport(runtime: StructuredOutputRuntime): { value?: unknown; error?: string } {
181
+ if (!runtime.acceptanceReportPath || !fs.existsSync(runtime.acceptanceReportPath)) return {};
182
+ try {
183
+ return { value: JSON.parse(fs.readFileSync(runtime.acceptanceReportPath, "utf-8")) };
184
+ } catch (error) {
185
+ return { error: `Failed to read structured output acceptance report: ${error instanceof Error ? error.message : String(error)}` };
186
+ }
187
+ }
188
+
175
189
  export function cleanupStructuredOutputRuntime(runtime: StructuredOutputRuntime | undefined): void {
176
190
  if (!runtime) return;
177
191
  try {
@@ -8,7 +8,7 @@ import { decodePermissionRules, permissionDecision, PERMISSION_AUDIT_PATH_ENV, P
8
8
  import { consumeSteerRequestsFromDir, MAX_STEER_QUEUE_SIZE, steerAckPathFromDir, writeSteerAckAt, writeSteerCapabilityAt, writeSteerRequestToDir, type SteerDeliveryStatus, type SteerRequest } from "../background/control-channel.ts";
9
9
  import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
10
10
  import { RUNTIME_EXTENSION_ACK_EVENT, RUNTIME_EXTENSION_ACK_PATH_ENV, isRuntimeAcknowledgedExtensionId, writeRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
11
- import { createStructuredOutputToolParameters, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
11
+ import { createStructuredOutputToolParameters, STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
12
12
  import {
13
13
  CHILD_TOOL_DIAGNOSTIC_PATH_ENV,
14
14
  formatChildToolDiagnostic,
@@ -634,29 +634,33 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
634
634
  await drainOutstandingWork({ state: waitState, events: pi.events });
635
635
  });
636
636
  const structuredOutputPath = process.env[STRUCTURED_OUTPUT_CAPTURE_ENV];
637
+ const structuredAcceptanceReportPath = process.env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV];
637
638
  const structuredSchemaPath = process.env[STRUCTURED_OUTPUT_SCHEMA_ENV];
638
639
  if (structuredOutputPath && structuredSchemaPath) {
639
640
  const schema = JSON.parse(fs.readFileSync(structuredSchemaPath, "utf-8")) as JsonSchemaObject;
640
- const parameters = createStructuredOutputToolParameters(schema);
641
+ const parameters = createStructuredOutputToolParameters(schema, { acceptanceReport: Boolean(structuredAcceptanceReportPath) });
641
642
  const registerTool = pi.registerTool as unknown as (tool: {
642
643
  name: string;
643
644
  label: string;
644
645
  description: string;
645
646
  parameters: unknown;
646
- execute: (_id: string, params: { value: unknown }) => Promise<unknown>;
647
+ execute: (_id: string, params: { value: unknown; acceptanceReport?: unknown }) => Promise<unknown>;
647
648
  }) => void;
648
649
  registerTool({
649
650
  name: "structured_output",
650
651
  label: "Structured Output",
651
652
  description: "Submit the required final structured output for this subagent step. This terminates the step.",
652
- parameters: parameters as never,
653
- async execute(_id: string, params: { value: unknown }) {
653
+ parameters,
654
+ async execute(_id: string, params: { value: unknown; acceptanceReport?: unknown }) {
654
655
  const validation = await validateStructuredOutputValue(schema, params.value);
655
656
  if (validation.status === "invalid") {
656
657
  throw new Error(`Structured output validation failed: ${validation.message}`);
657
658
  }
658
659
  fs.mkdirSync(path.dirname(structuredOutputPath), { recursive: true });
659
660
  fs.writeFileSync(structuredOutputPath, JSON.stringify(params.value), { mode: 0o600 });
661
+ if (structuredAcceptanceReportPath && params.acceptanceReport !== undefined) {
662
+ fs.writeFileSync(structuredAcceptanceReportPath, JSON.stringify(params.acceptanceReport), { mode: 0o600 });
663
+ }
660
664
  return {
661
665
  content: [{ type: "text", text: "Structured output captured." }],
662
666
  details: { path: structuredOutputPath },
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import type { AgentConfig } from "../agents/agents.ts";
4
+ import type { ExtensionBindings } from "../runs/shared/extension-bindings.ts";
4
5
 
5
6
  export const AGENT_DEFINITION_PROJECTION_VERSION = 1 as const;
6
7
  export const LAUNCH_BINDING_PROJECTION_VERSION = 1 as const;
@@ -46,6 +47,7 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
46
47
  model: agent.model,
47
48
  modelProvider: agent.modelProvider,
48
49
  fallbackModels: agent.fallbackModels,
50
+ fast: agent.fast,
49
51
  thinking: agent.thinking,
50
52
  tools: agent.tools,
51
53
  mcpDirectTools: agent.mcpDirectTools,
@@ -80,6 +82,7 @@ export interface LaunchBindingInput {
80
82
  task?: string;
81
83
  model?: string;
82
84
  modelCandidates?: string[];
85
+ fast?: boolean;
83
86
  thinking?: string;
84
87
  systemPrompt?: string | null;
85
88
  systemPromptMode?: AgentConfig["systemPromptMode"];
@@ -93,6 +96,7 @@ export interface LaunchBindingInput {
93
96
  outputPath?: string;
94
97
  outputMode?: string;
95
98
  structuredOutputSchema?: unknown;
99
+ extensionBindings?: ExtensionBindings;
96
100
  }
97
101
 
98
102
  /** Canonical projection of the resolved inputs handed to the child. */
@@ -104,6 +108,7 @@ export function projectLaunchBinding(input: LaunchBindingInput): Record<string,
104
108
  // The ordered candidate set already contains each attempted model; keeping only
105
109
  // this set makes retries correlate to the same preflight binding.
106
110
  modelCandidates: input.modelCandidates,
111
+ fast: input.fast,
107
112
  thinking: input.thinking,
108
113
  systemPromptDigest: input.systemPrompt === undefined || input.systemPrompt === null ? undefined : stableJsonDigest(input.systemPrompt),
109
114
  systemPromptMode: input.systemPromptMode,
@@ -117,6 +122,7 @@ export function projectLaunchBinding(input: LaunchBindingInput): Record<string,
117
122
  outputPath: input.outputPath,
118
123
  outputMode: input.outputMode,
119
124
  structuredOutputSchema: input.structuredOutputSchema,
125
+ extensionBindings: input.extensionBindings,
120
126
  };
121
127
  }
122
128
 
@@ -22,6 +22,7 @@ export interface ResolvedStepBehavior {
22
22
  progress: boolean;
23
23
  skills: string[] | false;
24
24
  model?: string;
25
+ fast?: boolean;
25
26
  }
26
27
 
27
28
  export type OutputOverrideInput = string | boolean;
@@ -33,6 +34,7 @@ export interface StepOverrides {
33
34
  progress?: boolean;
34
35
  skills?: string[] | false;
35
36
  model?: string;
37
+ fast?: boolean;
36
38
  }
37
39
 
38
40
  function normalizeOutputOverride(output: unknown): string | false | undefined {
@@ -60,6 +62,7 @@ export interface SequentialStep {
60
62
  progress?: boolean;
61
63
  skill?: string | string[] | false;
62
64
  model?: string;
65
+ fast?: boolean;
63
66
  toolBudget?: ToolBudgetConfig;
64
67
  acceptance?: AcceptanceInput;
65
68
  agentContract?: AgentContract;
@@ -84,6 +87,7 @@ export interface ParallelTaskItem {
84
87
  progress?: boolean;
85
88
  skill?: string | string[] | false;
86
89
  model?: string;
90
+ fast?: boolean;
87
91
  toolBudget?: ToolBudgetConfig;
88
92
  acceptance?: AcceptanceInput;
89
93
  agentContract?: AgentContract;
@@ -284,7 +288,8 @@ export function resolveStepBehavior(
284
288
 
285
289
  const outputMode = stepOverrides.outputMode ?? agentConfig.outputMode ?? "inline";
286
290
  const model = stepOverrides.model ?? agentConfig.model;
287
- return { output, outputMode, reads, progress, skills, model };
291
+ const fast = stepOverrides.fast ?? agentConfig.fast;
292
+ return { output, outputMode, reads, progress, skills, model, fast };
288
293
  }
289
294
 
290
295
  export function resolveTaskTextForFileUpdatePolicy(task: string | undefined, originalTask?: string): string | undefined {
@@ -13,6 +13,7 @@ import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from
13
13
  import type { AuthorityPolicyConfig } from "../policy/authority.ts";
14
14
  import type { ThinkingLevel } from "./model-info.ts";
15
15
  import type { GlobalMissionIndexRecord, MissionRecord, MissionStoreConfig } from "../missions/types.ts";
16
+ import type { ExtensionBindings } from "../runs/shared/extension-bindings.ts";
16
17
 
17
18
  // ============================================================================
18
19
  // Basic Types
@@ -348,12 +349,49 @@ export interface FileMutationEffect {
348
349
  attempted: boolean;
349
350
  message?: string;
350
351
  resolvedBy?: "llm-intent-arbiter";
352
+ evidence?: TrackedMutationEvidence;
351
353
  }
352
354
 
353
355
  export interface EffectsProjection {
354
356
  fileMutation?: FileMutationEffect;
355
357
  }
356
358
 
359
+ export interface TrackedMutationSnapshot {
360
+ source: "tracked-files";
361
+ trackedOnly: true;
362
+ cwd: string;
363
+ gitRoot?: string;
364
+ dirtyFiles: string[];
365
+ fingerprints: Record<string, TrackedMutationFingerprint>;
366
+ truncated?: boolean;
367
+ unavailable?: string;
368
+ }
369
+
370
+ export type TrackedMutationFingerprint = { kind: "diff"; digest: string };
371
+
372
+ export interface TrackedMutationEvidence {
373
+ source: "tracked-files";
374
+ trackedOnly: true;
375
+ changedFiles: string[];
376
+ attemptedMutation: boolean;
377
+ truncated?: boolean;
378
+ unavailable?: string;
379
+ }
380
+
381
+ export interface TimeoutRecoverySummary {
382
+ termination: "timed-out" | "stopped";
383
+ changedFiles: string[];
384
+ truncated?: boolean;
385
+ currentTool?: string;
386
+ currentToolArgs?: string;
387
+ currentPath?: string;
388
+ sessionFile?: string;
389
+ transcriptPath?: string;
390
+ artifactPaths?: ArtifactPaths;
391
+ warning: string;
392
+ message: string;
393
+ }
394
+
357
395
  export const SUBAGENT_LIFECYCLE_ARTIFACT_VERSION = 3;
358
396
  export type SubagentLifecycleArtifactVersion = typeof SUBAGENT_LIFECYCLE_ARTIFACT_VERSION;
359
397
 
@@ -526,6 +564,7 @@ export interface RunFanoutRejection extends RunFanoutBudgetSnapshot {
526
564
  export interface SteeringRecoveryDescriptor {
527
565
  version: 1;
528
566
  launchContractDigest?: string;
567
+ extensionBindings?: ExtensionBindings;
529
568
  runFanoutBudget: RunFanoutBudgetDescriptor;
530
569
  sourceRunId: string;
531
570
  agentContract?: AgentContract;
@@ -536,6 +575,7 @@ export interface SteeringRecoveryDescriptor {
536
575
  modelProvider?: string;
537
576
  modelOverrideFromParent?: boolean;
538
577
  fallbackModels?: string[];
578
+ fast?: boolean;
539
579
  thinking?: string;
540
580
  thinkingCeiling?: ThinkingLevel;
541
581
  tools?: string[];
@@ -941,6 +981,7 @@ export interface SingleResult {
941
981
  context?: "fresh" | "fork";
942
982
  exitCode: number;
943
983
  processSignal?: string | null;
984
+ timeoutRecovery?: TimeoutRecoverySummary;
944
985
  detached?: boolean;
945
986
  detachedReason?: string;
946
987
  interrupted?: boolean;
@@ -1505,6 +1546,7 @@ export interface AsyncStatus {
1505
1546
  durationMs?: number;
1506
1547
  exitCode?: number | null;
1507
1548
  timedOut?: boolean;
1549
+ timeoutRecovery?: TimeoutRecoverySummary;
1508
1550
  stopped?: boolean;
1509
1551
  turnBudget?: TurnBudgetState;
1510
1552
  turnBudgetExceeded?: boolean;
@@ -1644,6 +1686,8 @@ export interface ForegroundResumeChild {
1644
1686
  acceptance?: AcceptanceLedger;
1645
1687
  agentContract?: AgentContract;
1646
1688
  launchContractDigest?: string;
1689
+ /** Private retained launch authority. Never project into status or result output. */
1690
+ extensionBindings?: ExtensionBindings;
1647
1691
  launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
1648
1692
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
1649
1693
  execution?: ExecutionProjection;
@@ -1944,6 +1988,8 @@ export interface RunSyncOptions {
1944
1988
  nestedRoute?: NestedRouteInfo;
1945
1989
  /** Override the agent's default model (format: "provider/id" or just "id") */
1946
1990
  modelOverride?: string;
1991
+ /** Opt into priority service tier for supported native OpenAI-Codex launches. */
1992
+ fast?: boolean;
1947
1993
  /** The override came from the running parent session, not configuration. */
1948
1994
  modelOverrideFromParent?: boolean;
1949
1995
  /** LLM intent arbiter for the completion mutation guard (rescues read-only review runs). */
@@ -1951,6 +1997,7 @@ export interface RunSyncOptions {
1951
1997
  /** Override the agent's default thinking level for this run */
1952
1998
  thinkingOverride?: AgentConfig["thinking"];
1953
1999
  thinkingCeiling?: ThinkingLevel;
2000
+ extensionBindings?: ExtensionBindings;
1954
2001
  /** Registry models available for heuristic bare-model resolution */
1955
2002
  availableModels?: Array<{ provider: string; id: string; fullId: string }>;
1956
2003
  /** Current parent-session provider to prefer for ambiguous bare model ids */
@@ -1963,6 +2010,7 @@ export interface RunSyncOptions {
1963
2010
  schema: JsonSchemaObject;
1964
2011
  schemaPath: string;
1965
2012
  outputPath: string;
2013
+ acceptanceReportPath?: string;
1966
2014
  };
1967
2015
  agentContract?: AgentContract;
1968
2016
  acceptance?: AcceptanceInput;