pi-subagents 0.55.0 → 0.57.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 (97) hide show
  1. package/CHANGELOG.md +81 -3
  2. package/agents/claude-code-writer.md +15 -0
  3. package/agents/claude-code.md +15 -0
  4. package/agents/codex-exec-writer.md +15 -0
  5. package/agents/codex-exec.md +15 -0
  6. package/agents/cursor-agent-writer.md +14 -0
  7. package/agents/cursor-agent.md +14 -0
  8. package/docs/agents.md +111 -15
  9. package/docs/configuration.md +37 -0
  10. package/docs/extension-api.md +2 -2
  11. package/docs/models.md +6 -0
  12. package/docs/observability.md +8 -7
  13. package/docs/tool-reference.md +11 -1
  14. package/docs/workflows.md +20 -0
  15. package/package.json +1 -1
  16. package/skills/pi-subagents/references/execution-controls.md +1 -1
  17. package/src/agents/agent-management.ts +13 -5
  18. package/src/agents/agent-refinements.ts +4 -4
  19. package/src/agents/agent-serializer.ts +2 -0
  20. package/src/agents/agents.ts +242 -49
  21. package/src/agents/builtin-names.ts +6 -0
  22. package/src/agents/runtime-agent-registry.ts +8 -2
  23. package/src/api/preflight.ts +22 -3
  24. package/src/extension/config.ts +70 -0
  25. package/src/extension/doctor.ts +3 -3
  26. package/src/extension/index.ts +18 -4
  27. package/src/extension/public-execution.ts +29 -13
  28. package/src/extension/rpc.ts +20 -5
  29. package/src/extension/schemas.ts +9 -3
  30. package/src/extension/tool-description.ts +11 -9
  31. package/src/inspectors/herdr/actions.ts +2 -1
  32. package/src/inspectors/herdr/inspector-runner.ts +2 -10
  33. package/src/inspectors/herdr/session-roots-codec.ts +42 -0
  34. package/src/integrations/herdr-status.ts +46 -3
  35. package/src/runs/background/active-async-capacity.ts +77 -10
  36. package/src/runs/background/async-execution.ts +148 -27
  37. package/src/runs/background/async-job-tracker.ts +5 -0
  38. package/src/runs/background/async-resume.ts +3 -0
  39. package/src/runs/background/async-retention.ts +20 -3
  40. package/src/runs/background/async-status.ts +5 -0
  41. package/src/runs/background/chain-root-attachment.ts +15 -1
  42. package/src/runs/background/fleet-view.ts +16 -10
  43. package/src/runs/background/inspect-rpc.ts +8 -8
  44. package/src/runs/background/result-files.ts +27 -14
  45. package/src/runs/background/result-watcher.ts +9 -5
  46. package/src/runs/background/run-status.ts +33 -6
  47. package/src/runs/background/scheduled-runs.ts +7 -1
  48. package/src/runs/background/subagent-runner.ts +254 -49
  49. package/src/runs/background/wait-completions.ts +4 -0
  50. package/src/runs/foreground/execution.ts +107 -16
  51. package/src/runs/foreground/foreground-control.ts +6 -0
  52. package/src/runs/foreground/foreground-history.ts +25 -3
  53. package/src/runs/foreground/subagent-executor.ts +324 -84
  54. package/src/runs/foreground/workflow-detach-reconcile.ts +31 -7
  55. package/src/runs/shared/acceptance.ts +10 -5
  56. package/src/runs/shared/agent-contract.ts +1 -1
  57. package/src/runs/shared/child-protocol.ts +21 -7
  58. package/src/runs/shared/claude-code-adapter.ts +129 -0
  59. package/src/runs/shared/codex-exec-adapter.ts +129 -0
  60. package/src/runs/shared/completion-guard.ts +3 -2
  61. package/src/runs/shared/cursor-agent-adapter.ts +114 -0
  62. package/src/runs/shared/dynamic-fanout.ts +1 -1
  63. package/src/runs/shared/extension-bindings.ts +78 -0
  64. package/src/runs/shared/external-cli-contract.ts +167 -0
  65. package/src/runs/shared/external-cli-preflight.ts +122 -0
  66. package/src/runs/shared/external-cli-runner.ts +349 -54
  67. package/src/runs/shared/fast-mode-extension.ts +10 -0
  68. package/src/runs/shared/model-exclusions.ts +59 -8
  69. package/src/runs/shared/model-fallback.ts +25 -8
  70. package/src/runs/shared/mutation-evidence.ts +150 -0
  71. package/src/runs/shared/nested-events.ts +3 -1
  72. package/src/runs/shared/nested-render.ts +2 -2
  73. package/src/runs/shared/parallel-utils.ts +3 -0
  74. package/src/runs/shared/pi-args.ts +47 -0
  75. package/src/runs/shared/process-signal.ts +13 -0
  76. package/src/runs/shared/run-history.ts +21 -1
  77. package/src/runs/shared/structured-output.ts +18 -4
  78. package/src/runs/shared/subagent-prompt-runtime.ts +9 -5
  79. package/src/shared/fork-context.ts +21 -0
  80. package/src/shared/formatters.ts +7 -1
  81. package/src/shared/launch-contract.ts +6 -0
  82. package/src/shared/pruned-fork.ts +450 -0
  83. package/src/shared/session-file-trust.ts +19 -0
  84. package/src/shared/session-tokens.ts +14 -3
  85. package/src/shared/settings.ts +16 -3
  86. package/src/shared/shortcuts.ts +17 -0
  87. package/src/shared/types.ts +166 -9
  88. package/src/shared/workflow-child-permit.ts +116 -0
  89. package/src/slash/delegation-adapters.ts +0 -1
  90. package/src/slash/slash-commands.ts +8 -6
  91. package/src/tui/fleet-status.ts +27 -10
  92. package/src/tui/fleet-transcript.ts +11 -5
  93. package/src/tui/fleet.ts +28 -13
  94. package/src/tui/render.ts +47 -21
  95. package/src/workflows/scripted-workflow.ts +298 -30
  96. package/src/workflows/workflow-child-summary.ts +117 -0
  97. package/src/workflows/workflow-receipt.ts +119 -4
@@ -0,0 +1,150 @@
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 gitArguments(args: string[]): string[] {
13
+ // Mutation evidence must not block child startup on a stale fsmonitor daemon.
14
+ return ["-c", "core.fsmonitor=false", ...args];
15
+ }
16
+
17
+ function gitOutput(cwd: string, args: string[], maxBuffer = MAX_HASH_BYTES): string {
18
+ return execFileSync("git", gitArguments(args), { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer });
19
+ }
20
+
21
+ function splitNul(output: string): string[] {
22
+ return output.split("\0").filter((part) => part.length > 0);
23
+ }
24
+
25
+ function hashLargeDiff(cwd: string, relativePath: string): string {
26
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tracked-diff-"));
27
+ const diffPath = path.join(tempDir, "diff.patch");
28
+ try {
29
+ execFileSync("git", gitArguments(["diff", "--no-ext-diff", "--binary", `--output=${diffPath}`, "HEAD", "--", relativePath]), { cwd, stdio: "ignore" });
30
+ const hash = createHash("sha256");
31
+ const buffer = Buffer.allocUnsafe(64 * 1024);
32
+ const fd = fs.openSync(diffPath, "r");
33
+ try {
34
+ for (;;) {
35
+ const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null);
36
+ if (bytesRead === 0) break;
37
+ hash.update(buffer.subarray(0, bytesRead));
38
+ }
39
+ } finally {
40
+ fs.closeSync(fd);
41
+ }
42
+ return hash.digest("hex");
43
+ } finally {
44
+ fs.rmSync(tempDir, { recursive: true, force: true });
45
+ }
46
+ }
47
+
48
+ function listChangedTrackedFiles(cwd: string): { paths: string[]; truncated: boolean } {
49
+ const paths = splitNul(gitOutput(cwd, ["diff", "--no-ext-diff", "--name-only", "-z", "HEAD", "--"], MAX_HASH_BYTES));
50
+ return { paths: paths.slice(0, MAX_TRACKED_PATHS), truncated: paths.length > MAX_TRACKED_PATHS };
51
+ }
52
+
53
+ function fingerprintPath(cwd: string, relativePath: string): TrackedMutationFingerprint {
54
+ try {
55
+ const diff = gitOutput(cwd, ["diff", "--no-ext-diff", "--binary", "HEAD", "--", relativePath], MAX_HASH_BYTES);
56
+ return { kind: "diff", digest: createHash("sha256").update(diff).digest("hex") };
57
+ } catch {
58
+ return { kind: "diff", digest: hashLargeDiff(cwd, relativePath) };
59
+ }
60
+ }
61
+
62
+ function sameFingerprint(left: TrackedMutationFingerprint | undefined, right: TrackedMutationFingerprint): boolean {
63
+ if (!left || left.kind !== "diff" || right.kind !== "diff") return false;
64
+ return left.digest === right.digest;
65
+ }
66
+
67
+ export function snapshotTrackedMutations(cwd: string): TrackedMutationSnapshot {
68
+ try {
69
+ const changed = listChangedTrackedFiles(cwd);
70
+ const fingerprints: Record<string, TrackedMutationFingerprint> = {};
71
+ for (const file of changed.paths) fingerprints[file] = fingerprintPath(cwd, file);
72
+ return { source: "tracked-files", trackedOnly: true, cwd, dirtyFiles: changed.paths, fingerprints, truncated: changed.truncated };
73
+ } catch (error) {
74
+ return { source: "tracked-files", trackedOnly: true, cwd, dirtyFiles: [], fingerprints: {}, unavailable: error instanceof Error ? error.message : String(error) };
75
+ }
76
+ }
77
+
78
+ export function collectTrackedMutationEvidence(snapshot: TrackedMutationSnapshot, cwd = snapshot.cwd): TrackedMutationEvidence {
79
+ if (snapshot.unavailable) {
80
+ return { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: false, unavailable: snapshot.unavailable };
81
+ }
82
+ try {
83
+ const current = listChangedTrackedFiles(cwd);
84
+ const startDirty = new Set(snapshot.dirtyFiles);
85
+ const candidates = new Set([...snapshot.dirtyFiles, ...current.paths]);
86
+ const changedFiles: string[] = [];
87
+ for (const file of candidates) {
88
+ if (!startDirty.has(file)) {
89
+ if (!snapshot.truncated) changedFiles.push(file);
90
+ continue;
91
+ }
92
+ if (!sameFingerprint(snapshot.fingerprints[file], fingerprintPath(cwd, file))) changedFiles.push(file);
93
+ }
94
+ changedFiles.sort();
95
+ return {
96
+ source: "tracked-files",
97
+ trackedOnly: true,
98
+ changedFiles,
99
+ attemptedMutation: changedFiles.length > 0,
100
+ truncated: snapshot.truncated || current.truncated || undefined,
101
+ };
102
+ } catch (error) {
103
+ return { source: "tracked-files", trackedOnly: true, changedFiles: [], attemptedMutation: false, unavailable: error instanceof Error ? error.message : String(error) };
104
+ }
105
+ }
106
+
107
+ function formatPathList(paths: string[]): string {
108
+ if (paths.length === 0) return "none";
109
+ const shown = paths.slice(0, MAX_TIMEOUT_FILES).join(", ");
110
+ return paths.length > MAX_TIMEOUT_FILES ? `${shown}, ... (${paths.length - MAX_TIMEOUT_FILES} more)` : shown;
111
+ }
112
+
113
+ export function buildTimeoutRecoverySummary(input: {
114
+ termination: "timed-out" | "stopped";
115
+ evidence: TrackedMutationEvidence;
116
+ currentTool?: string;
117
+ currentToolArgs?: string;
118
+ currentPath?: string;
119
+ sessionFile?: string;
120
+ transcriptPath?: string;
121
+ artifactPaths?: ArtifactPaths;
122
+ }): TimeoutRecoverySummary {
123
+ const warning = "Inspect partial changes before retrying or resuming the child.";
124
+ const changedFiles = input.evidence.changedFiles.slice(0, MAX_TIMEOUT_FILES);
125
+ const lines = [
126
+ "Recovery summary:",
127
+ `- termination: ${input.termination}`,
128
+ `- changed tracked files: ${input.evidence.unavailable ? `unavailable (${input.evidence.unavailable})` : formatPathList(input.evidence.changedFiles)}`,
129
+ ];
130
+ if (input.currentTool) lines.push(`- active tool: ${input.currentTool}${input.currentToolArgs ? ` — ${input.currentToolArgs}` : ""}`);
131
+ if (input.currentPath) lines.push(`- active path: ${input.currentPath}`);
132
+ if (input.sessionFile) lines.push(`- session file: ${input.sessionFile}`);
133
+ if (input.transcriptPath) lines.push(`- transcript: ${input.transcriptPath}`);
134
+ if (input.artifactPaths?.outputPath) lines.push(`- output artifact: ${input.artifactPaths.outputPath}`);
135
+ if (input.artifactPaths?.metadataPath) lines.push(`- metadata artifact: ${input.artifactPaths.metadataPath}`);
136
+ lines.push(`Warning: ${warning}`);
137
+ return {
138
+ termination: input.termination,
139
+ changedFiles,
140
+ truncated: input.evidence.changedFiles.length > changedFiles.length || input.evidence.truncated || undefined,
141
+ currentTool: input.currentTool,
142
+ currentToolArgs: input.currentToolArgs,
143
+ currentPath: input.currentPath,
144
+ sessionFile: input.sessionFile,
145
+ transcriptPath: input.transcriptPath,
146
+ artifactPaths: input.artifactPaths,
147
+ warning,
148
+ message: lines.join("\n"),
149
+ };
150
+ }
@@ -199,8 +199,10 @@ function sanitizeTokenUsage(value: unknown): NestedRunSummary["totalTokens"] | u
199
199
  const input = clampNumber(raw.input);
200
200
  const output = clampNumber(raw.output);
201
201
  const total = clampNumber(raw.total);
202
+ const window = clampNumber(raw.window);
203
+ const windowPeak = clampNumber(raw.windowPeak);
202
204
  return input !== undefined && output !== undefined && total !== undefined
203
- ? { input, output, total }
205
+ ? { input, output, total, ...(window !== undefined ? { window } : {}), ...(windowPeak !== undefined ? { windowPeak } : {}) }
204
206
  : undefined;
205
207
  }
206
208
 
@@ -1,4 +1,4 @@
1
- import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
1
+ import { formatDuration, formatModelThinking, formatTokenUsage, shortenPath } from "../../shared/formatters.ts";
2
2
  import { formatActivityLabel } from "../../shared/status-format.ts";
3
3
  import type { ActivityState, NestedRunSummary, NestedStepSummary } from "../../shared/types.ts";
4
4
 
@@ -68,7 +68,7 @@ function formatNestedActivity(input: {
68
68
  if (input.currentPath) facts.push(shortenPath(input.currentPath));
69
69
  if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
70
70
  if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
71
- if (input.totalTokens) facts.push(`${formatTokens(input.totalTokens.total)} tok`);
71
+ if (input.totalTokens) facts.push(formatTokenUsage(input.totalTokens));
72
72
  const activity = formatActivityLabel(input.lastActivityAt, input.activityState as ActivityState | undefined);
73
73
  return activity || facts.length ? [activity, ...facts].filter(Boolean).join(" | ") : undefined;
74
74
  }
@@ -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;
@@ -2,6 +2,19 @@ export function formatProcessSignalError(signal: string): string {
2
2
  return `Subagent process terminated by signal ${signal}.`;
3
3
  }
4
4
 
5
+ export function formatMidToolExitError(input: {
6
+ toolName: string;
7
+ exitCode: number | null;
8
+ processSignal?: string | null;
9
+ }): string {
10
+ const terminal = [`exit ${input.exitCode ?? "unknown"}`, ...(input.processSignal ? [`signal ${input.processSignal}`] : [])].join(", ");
11
+ return `Subagent process exited during '${input.toolName}' tool execution (${terminal}) before the tool completed. Earlier assistant output is not a terminal result.`;
12
+ }
13
+
14
+ export function isOrdinaryToolForMidToolExit(toolName: string): boolean {
15
+ return toolName !== "intercom" && toolName !== "contact_supervisor";
16
+ }
17
+
5
18
  export function isUnexplainedProcessSignal(input: {
6
19
  processSignal?: string | null;
7
20
  interrupted?: boolean;
@@ -2,6 +2,9 @@ import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { getAgentDir } from "../../shared/utils.ts";
5
+ import { isUnexplainedProcessSignal } from "./process-signal.ts";
6
+
7
+ export type RunOutcome = "completed" | "failed" | "timed_out" | "stopped" | "interrupted";
5
8
 
6
9
  export interface RunEntry {
7
10
  agent: string;
@@ -9,6 +12,7 @@ export interface RunEntry {
9
12
  taskHash?: string;
10
13
  ts: number;
11
14
  status: "ok" | "error";
15
+ outcome?: RunOutcome;
12
16
  duration: number;
13
17
  exit?: number;
14
18
  }
@@ -129,14 +133,30 @@ function appendPrivateHistoryLine(historyPath: string, line: string): void {
129
133
  }
130
134
  }
131
135
 
132
- export function recordRun(agent: string, task: string, exitCode: number, durationMs: number): void {
136
+ export function recordRun(
137
+ agent: string,
138
+ task: string,
139
+ exitCode: number,
140
+ durationMs: number,
141
+ terminal: { interrupted?: boolean; processSignal?: string | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean } = {},
142
+ ): void {
133
143
  try {
144
+ const outcome: RunOutcome = terminal.stopped
145
+ ? "stopped"
146
+ : terminal.interrupted
147
+ ? "interrupted"
148
+ : terminal.timedOut
149
+ ? "timed_out"
150
+ : exitCode !== 0 && isUnexplainedProcessSignal(terminal)
151
+ ? "stopped"
152
+ : exitCode === 0 ? "completed" : "failed";
134
153
  const entry: RunEntry = {
135
154
  agent,
136
155
  task: REDACTED_TASK,
137
156
  taskHash: hashTask(task),
138
157
  ts: Math.floor(Date.now() / 1000),
139
158
  status: exitCode === 0 ? "ok" : "error",
159
+ outcome,
140
160
  duration: durationMs,
141
161
  ...(exitCode !== 0 ? { exit: exitCode } : {}),
142
162
  };
@@ -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 },
@@ -37,6 +37,8 @@ interface ForkableSessionManager {
37
37
 
38
38
  interface ForkContextResolverOptions {
39
39
  openSession?: (path: string, sessionDir?: string) => BranchSessionManager;
40
+ /** Rewrite a created fork before its path can be used to spawn a child. */
41
+ pruneSession?: (sessionFile: string) => Promise<void>;
40
42
  /** Decide per child index whether a sanitized transcript must also disable the child's
41
43
  * thinking. Defaults to true (the pre-existing conservative behavior) when omitted. */
42
44
  forceThinkingOffForIndex?: (index: number) => boolean;
@@ -48,6 +50,7 @@ interface ForkContextResolution {
48
50
  }
49
51
 
50
52
  interface ForkContextResolver {
53
+ prepareSessionForIndex(index?: number): Promise<void>;
51
54
  sessionFileForIndex(index?: number): string | undefined;
52
55
  thinkingOverrideForIndex(index?: number): "off" | undefined;
53
56
  }
@@ -190,6 +193,7 @@ export function createForkContextResolver(
190
193
  ): ForkContextResolver {
191
194
  if (resolveSubagentContext(requestedContext) !== "fork") {
192
195
  return {
196
+ prepareSessionForIndex: async () => {},
193
197
  sessionFileForIndex: () => undefined,
194
198
  thinkingOverrideForIndex: () => undefined,
195
199
  };
@@ -226,6 +230,8 @@ export function createForkContextResolver(
226
230
  "forks",
227
231
  );
228
232
  const cachedResolutions = new Map<number, ForkContextResolution>();
233
+ const preparedIndexes = new Set<number>();
234
+ const preparationPromises = new Map<number, Promise<void>>();
229
235
 
230
236
  const resolveFork = (index = 0): ForkContextResolution => {
231
237
  const cached = cachedResolutions.get(index);
@@ -274,7 +280,22 @@ export function createForkContextResolver(
274
280
  };
275
281
 
276
282
  return {
283
+ async prepareSessionForIndex(index = 0): Promise<void> {
284
+ const resolution = resolveFork(index);
285
+ if (!options.pruneSession || preparedIndexes.has(index)) return;
286
+ let preparation = preparationPromises.get(index);
287
+ if (!preparation) {
288
+ preparation = options.pruneSession(resolution.sessionFile).then(() => {
289
+ preparedIndexes.add(index);
290
+ });
291
+ preparationPromises.set(index, preparation);
292
+ }
293
+ await preparation;
294
+ },
277
295
  sessionFileForIndex(index = 0): string | undefined {
296
+ if (options.pruneSession && !preparedIndexes.has(index)) {
297
+ throw new Error(`Pruned fork session ${index} was used before pruning completed.`);
298
+ }
278
299
  return resolveFork(index).sessionFile;
279
300
  },
280
301
  thinkingOverrideForIndex(index = 0): "off" | undefined {
@@ -4,7 +4,7 @@
4
4
 
5
5
  import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
- import type { Usage, SingleResult } from "./types.ts";
7
+ import type { Usage, SingleResult, TokenUsage } from "./types.ts";
8
8
  import type { ChainStep } from "./settings.ts";
9
9
  import { isDynamicParallelStep, isParallelStep } from "./settings.ts";
10
10
  import { previewDisplayText, sanitizeDisplayText } from "./display-text.ts";
@@ -17,6 +17,12 @@ export function formatTokens(n: number): string {
17
17
  return n < 1000 ? String(n) : n < 10000 ? `${(n / 1000).toFixed(1)}k` : `${Math.round(n / 1000)}k`;
18
18
  }
19
19
 
20
+ export function formatTokenUsage(usage: TokenUsage, legacyLabel = "tok"): string {
21
+ return usage.window !== undefined
22
+ ? `${formatTokens(usage.window)} window · ${formatTokens(usage.total)} spent`
23
+ : `${formatTokens(usage.total)} ${legacyLabel}`;
24
+ }
25
+
20
26
  export function formatModelThinking(model?: string, thinking?: string): string {
21
27
  const parsed = model ? splitKnownThinkingSuffix(model) : undefined;
22
28
  let displayModel = parsed?.baseModel ?? model;
@@ -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