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
package/CHANGELOG.md CHANGED
@@ -1,10 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [0.56.0] - 2026-08-23
6
+
7
+ ### Highlights
8
+ - Run allowlisted OpenAI-Codex subagents with opt-in `fast` mode when you want priority service tier.
9
+ - Pass bounded extension metadata into native child launches without leaking that authority to external runners.
10
+ - Workflow scripts are easier to read when child results are stringified or returned.
11
+ - Completion guards now use safer tracked-file evidence, including large dirty files and interrupted runs.
12
+ - Model verification is less fragile for provider-qualified and variant-tagged model ids.
13
+
14
+ ### Added
15
+ - Add opt-in `fast: true` launches for allowlisted native OpenAI-Codex subagents, using OpenAI's priority service tier.
16
+ - Add bounded namespaced extension bindings to child launch contracts. Thanks to [@FL03](https://github.com/FL03) for #1410.
17
+
18
+ ### Fixed
19
+ - Render workflow child results as useful text when scripts stringify `runs.all` or awaited `runs.run` result objects.
20
+ - Keep checked acceptance compatible with strict workflow child `outputSchema` results. Thanks to [@rtbe](https://github.com/rtbe) for #1406.
21
+ - Use bounded tracked-file mutation evidence for implementation completion guards, including files that were already dirty when the child started. Thanks to [@rtbe](https://github.com/rtbe) for #1407.
22
+ - Bind fast mode into launch contract provenance and keep large tracked-file mutation evidence precise.
23
+ - Add timeout recovery summaries with changed tracked files, active child state, and session/artifact paths. Thanks to [@rtbe](https://github.com/rtbe) for #1409.
24
+ - Fail closed when reviewer runs are interrupted or detached workflow children settle without persisted top-level continuation proof. Thanks to [@rtbe](https://github.com/rtbe) for #1408.
25
+ - Stop flagging awaited `.then()` workflow chains as unawaited when a handler returns another child launch.
26
+ - Preserve variant-tagged model ids during verification and fallback exclusion parsing. Thanks to [@rafafortes](https://github.com/rafafortes) for #1420.
27
+
3
28
  ## [0.55.0] - 2026-08-23
4
29
 
5
30
  ### Highlights
6
31
  - Stop a single stuck child in an async workflow without stopping the whole run.
7
- - Continue finished external jobs, like Surf's `gpt-pro`, with follow-up requests through `resume`.
32
+ - Continue finished external jobs, like `gpt-pro` from [Surf](https://github.com/nicobailon/surf-cli/), with follow-up requests through `resume`.
8
33
  - Cap child thinking with `subagents.maxThinking` and set a preferred default provider for bare model ids.
9
34
  - Scripted workflow outputs now land in the run's managed artifact directory instead of the repository root.
10
35
  - Child launches fail fast with clear reasons when requested models or write tools are unavailable.
@@ -31,10 +56,10 @@
31
56
  - Fail child launch attempts when the runtime lacks requested core write tools or an implementation worker has only read-only launch tools, including workflow children that inherit a read-only capability ceiling.
32
57
  - Let read-only reviewer acceptance rely on the parent-side staged-file check instead of requiring child-reported `noStagedFiles` evidence.
33
58
  - Run public single-child launches directly instead of wrapping them in a workflow, so async external-job agents do not show a completed workflow before the real provider job finishes.
34
- - Start omitted-`async` public external-runner single-child launches in the supported background mode, so package agents such as Surf's `gpt-pro` do not fail as foreground requests.
59
+ - Start omitted-`async` public external-runner single-child launches in the supported background mode, so package agents such as `gpt-pro` from [Surf](https://github.com/nicobailon/surf-cli/) do not fail as foreground requests.
35
60
  - Let workflow scripts await omitted-`async` external-runner children by launching them in the background internally and returning their terminal result.
36
61
  - Report helpful workflow errors when `runs.all(...)` results are read as keyed objects instead of ordered arrays. Thanks to [@ravshansbox](https://github.com/ravshansbox) for #1351.
37
- - Clarify that Council Mode can include installed external-runner advisors such as Surf's `gpt-pro` when the `surf-cli` Pi extension has registered `surf-oracle`, with text JSON reports instead of `outputSchema`.
62
+ - Clarify that Council Mode can include installed external-runner advisors such as `gpt-pro` from [Surf](https://github.com/nicobailon/surf-cli/) when the `surf-cli` Pi extension has registered `surf-oracle`, with text JSON reports instead of `outputSchema`.
38
63
 
39
64
  ## [0.54.0] - 2026-08-21
40
65
 
package/docs/models.md CHANGED
@@ -60,6 +60,12 @@ For a persistent role override with a backup model for provider failures:
60
60
 
61
61
  `subagents.defaultModel` and `subagents.defaultProvider` apply to builtin, package, user, and project agents. `defaultModel` fills only agents that do not set `model` in frontmatter. `defaultProvider` is also applied to frontmatter and override models so bare ids resolve against the intended provider. Per-run model overrides and `agentOverrides.<name>.model` still win, and explicit agent frontmatter still wins over the global default. The same `agentOverrides` block can change `tools`, `skills`, inherited context, prompt text, or disable a builtin (see [agents.md](agents.md)). Matching user and project agents also receive override fields that their frontmatter leaves unset, so a shared project config agent can keep the persona while local settings choose the model or provider.
62
62
 
63
+ ## Fast mode
64
+
65
+ Set `fast: true` on a run, in agent frontmatter, or in `subagents.agentOverrides.<name>.fast` to request the OpenAI priority service tier for supported native OpenAI-Codex children. This can use a higher quota tier or cost more. It is off by default.
66
+
67
+ Fast mode fails before launch unless every resolved model candidate is on the allowlist. The current allowlist is `openai-codex/gpt-5.6-luna` and `openai-codex/gpt-5.6-sol`. External runners, Anthropic models, and other providers do not use fast mode.
68
+
63
69
  ## Recommended model tiering (optional)
64
70
 
65
71
  A setup that works well in practice: route agents by task shape instead of running everything on one model. Four tiers:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.55.0",
3
+ "version": "0.56.0",
4
4
  "description": "Pi extension for single-agent delegation and scripted multi-agent workflows",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -11,6 +11,7 @@ export const KNOWN_FIELDS = new Set([
11
11
  "tools",
12
12
  "model",
13
13
  "fallbackModels",
14
+ "fast",
14
15
  "thinking",
15
16
  "systemPromptMode",
16
17
  "inheritProjectContext",
@@ -71,6 +72,7 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
71
72
  if (config.model || preserve("model")) lines.push(`model: ${config.model ?? ""}`);
72
73
  const fallbackModelsValue = joinComma(config.fallbackModels);
73
74
  if (fallbackModelsValue || preserve("fallbackModels")) lines.push(`fallbackModels: ${fallbackModelsValue ?? ""}`);
75
+ if (config.fast === true || preserve("fast")) lines.push(`fast: ${config.fast === undefined ? "" : config.fast ? "true" : "false"}`);
74
76
  if ((config.thinking && (config.thinking !== "off" || preserve("thinking"))) || (!config.thinking && preserve("thinking"))) {
75
77
  lines.push(`thinking: ${config.thinking ?? ""}`);
76
78
  }
@@ -57,6 +57,7 @@ export interface BuiltinAgentOverrideBase {
57
57
  model?: string;
58
58
  modelProvider?: string;
59
59
  fallbackModels?: string[];
60
+ fast?: boolean;
60
61
  thinking?: string | false;
61
62
  systemPromptMode: SystemPromptMode;
62
63
  inheritProjectContext: boolean;
@@ -83,6 +84,7 @@ interface BuiltinAgentOverrideConfig {
83
84
  model?: string | false;
84
85
  defaultProvider?: string | false;
85
86
  fallbackModels?: string[] | false;
87
+ fast?: boolean;
86
88
  thinking?: string | false;
87
89
  systemPromptMode?: SystemPromptMode;
88
90
  inheritProjectContext?: boolean;
@@ -128,6 +130,7 @@ export interface AgentConfig {
128
130
  model?: string;
129
131
  modelProvider?: string;
130
132
  fallbackModels?: string[];
133
+ fast?: boolean;
131
134
  thinking?: string | false;
132
135
  systemPromptMode: SystemPromptMode;
133
136
  inheritProjectContext: boolean;
@@ -640,6 +643,7 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
640
643
  ...(agent.model !== undefined ? { model: agent.model } : {}),
641
644
  ...(agent.modelProvider !== undefined ? { modelProvider: agent.modelProvider } : {}),
642
645
  ...(agent.fallbackModels ? { fallbackModels: [...agent.fallbackModels] } : {}),
646
+ ...(agent.fast !== undefined ? { fast: agent.fast } : {}),
643
647
  ...(agent.thinking !== undefined ? { thinking: agent.thinking } : {}),
644
648
  systemPromptMode: agent.systemPromptMode,
645
649
  inheritProjectContext: agent.inheritProjectContext,
@@ -670,6 +674,7 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
670
674
  ...(override.fallbackModels !== undefined
671
675
  ? { fallbackModels: override.fallbackModels === false ? false : [...override.fallbackModels] }
672
676
  : {}),
677
+ ...(override.fast !== undefined ? { fast: override.fast } : {}),
673
678
  ...(override.thinking !== undefined ? { thinking: override.thinking } : {}),
674
679
  ...(override.systemPromptMode !== undefined ? { systemPromptMode: override.systemPromptMode } : {}),
675
680
  ...(override.inheritProjectContext !== undefined ? { inheritProjectContext: override.inheritProjectContext } : {}),
@@ -864,6 +869,11 @@ function parseBuiltinOverrideEntry(
864
869
  else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'model'; expected a string or false.`);
865
870
  }
866
871
 
872
+ if ("fast" in input) {
873
+ if (typeof input.fast === "boolean") override.fast = input.fast;
874
+ else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'fast'; expected a boolean.`);
875
+ }
876
+
867
877
  if ("thinking" in input) {
868
878
  if (typeof input.thinking === "string" || input.thinking === false) override.thinking = input.thinking;
869
879
  else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'thinking'; expected a string or false.`);
@@ -1195,6 +1205,7 @@ function applyBuiltinOverride(
1195
1205
  else next.modelProvider = override.defaultProvider;
1196
1206
  }
1197
1207
  if (override.fallbackModels !== undefined) { if (override.fallbackModels === false) delete next.fallbackModels; else next.fallbackModels = [...override.fallbackModels]; }
1208
+ if (override.fast !== undefined) next.fast = override.fast;
1198
1209
  if (override.thinking !== undefined) { if (override.thinking === false) delete next.thinking; else next.thinking = override.thinking; }
1199
1210
  if (override.systemPromptMode !== undefined) next.systemPromptMode = override.systemPromptMode;
1200
1211
  if (override.inheritProjectContext !== undefined) next.inheritProjectContext = override.inheritProjectContext;
@@ -1332,6 +1343,9 @@ function applyCustomAgentOverride(
1332
1343
  override.fallbackModels === false ? undefined : [...override.fallbackModels],
1333
1344
  );
1334
1345
  }
1346
+ if (override.fast !== undefined) {
1347
+ fill("fast", ["fast"], override.fast);
1348
+ }
1335
1349
  if (override.thinking !== undefined) {
1336
1350
  fill("thinking", ["thinking"], override.thinking === false ? undefined : override.thinking);
1337
1351
  }
@@ -1411,7 +1425,7 @@ function applyCustomAgentOverrides(
1411
1425
 
1412
1426
  export function buildBuiltinOverrideConfig(
1413
1427
  base: BuiltinAgentOverrideBase,
1414
- draft: Pick<AgentConfig, "model" | "modelProvider" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "completionGuard" | "toolBudget"> & Partial<Pick<AgentConfig, "description" | "output" | "outputMode" | "defaultReads">>,
1428
+ draft: Pick<AgentConfig, "model" | "modelProvider" | "fallbackModels" | "fast" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "completionGuard" | "toolBudget"> & Partial<Pick<AgentConfig, "description" | "output" | "outputMode" | "defaultReads">>,
1415
1429
  ): BuiltinAgentOverrideConfig | undefined {
1416
1430
  const override: BuiltinAgentOverrideConfig = {};
1417
1431
 
@@ -1425,6 +1439,7 @@ export function buildBuiltinOverrideConfig(
1425
1439
  if (draft.model !== base.model) override.model = draft.model ?? false;
1426
1440
  if (draft.modelProvider !== base.modelProvider) override.defaultProvider = draft.modelProvider ?? false;
1427
1441
  if (!arraysEqual(draft.fallbackModels, base.fallbackModels)) override.fallbackModels = draft.fallbackModels ? [...draft.fallbackModels] : false;
1442
+ if (draft.fast !== base.fast) override.fast = draft.fast === true;
1428
1443
  if (draft.thinking !== base.thinking) override.thinking = draft.thinking ?? false;
1429
1444
  if (draft.systemPromptMode !== base.systemPromptMode) override.systemPromptMode = draft.systemPromptMode;
1430
1445
  if (draft.inheritProjectContext !== base.inheritProjectContext) override.inheritProjectContext = draft.inheritProjectContext;
@@ -1835,6 +1850,12 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
1835
1850
 
1836
1851
  const extensions = resolveAgentRelativeExtensionPaths(parseFrontmatterList(frontmatter.extensions), filePath);
1837
1852
  const subagentOnlyExtensions = resolveAgentRelativeExtensionPaths(parseFrontmatterList(frontmatter.subagentOnlyExtensions), filePath);
1853
+ let fast: boolean | undefined;
1854
+ if (frontmatter.fast !== undefined) {
1855
+ if (frontmatter.fast === "true") fast = true;
1856
+ else if (frontmatter.fast === "false") fast = false;
1857
+ else throw new Error(`Agent '${localName}' has invalid fast frontmatter; expected true or false.`);
1858
+ }
1838
1859
 
1839
1860
  const extraFields: Record<string, string> = {};
1840
1861
  for (const [key, value] of Object.entries(frontmatter)) {
@@ -1881,6 +1902,7 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
1881
1902
  ...(mcpDirectTools.length > 0 ? { mcpDirectTools } : {}),
1882
1903
  ...(frontmatter.model !== undefined ? { model: frontmatter.model } : {}),
1883
1904
  ...(fallbackModels?.length ? { fallbackModels } : {}),
1905
+ ...(fast !== undefined ? { fast } : {}),
1884
1906
  ...(frontmatter.thinking !== undefined ? { thinking: frontmatter.thinking === "false" ? false : frontmatter.thinking } : {}),
1885
1907
  systemPromptMode,
1886
1908
  inheritProjectContext,
@@ -26,6 +26,7 @@ import { DIRS, TEMP_ROOT_DIR } from "../shared/types.ts";
26
26
  import { processTerminalCandidatePath, processTerminalPath } from "../runs/background/process-terminal.ts";
27
27
  import { resultFilePath } from "../runs/background/result-files.ts";
28
28
  import { nestedResultsPath } from "../runs/shared/nested-events.ts";
29
+ import { normalizeExtensionBindings, type ExtensionBindings } from "../runs/shared/extension-bindings.ts";
29
30
 
30
31
  export const SUBAGENT_LAUNCH_CONTRACT_VERSION = 2 as const;
31
32
 
@@ -38,7 +39,8 @@ export type SubagentLaunchContractReasonCode =
38
39
  | "invalid_cwd"
39
40
  | "unsupported_mode"
40
41
  | "restricted_agent"
41
- | "thinking_ceiling";
42
+ | "thinking_ceiling"
43
+ | "invalid_extension_bindings";
42
44
 
43
45
  export interface SubagentLaunchContractDiagnostic {
44
46
  code: SubagentLaunchContractReasonCode | "host_required" | "snapshot_warning";
@@ -53,6 +55,7 @@ export interface SubagentLaunchContractInput {
53
55
  agentScope?: AgentScope;
54
56
  context?: "fresh" | "fork";
55
57
  model?: string;
58
+ fast?: boolean;
56
59
  thinking?: string | false;
57
60
  thinkingCeiling?: ThinkingLevel;
58
61
  inheritedThinkingCeiling?: ThinkingLevel;
@@ -63,6 +66,7 @@ export interface SubagentLaunchContractInput {
63
66
  output?: string | boolean;
64
67
  outputMode?: OutputMode;
65
68
  outputSchema?: JsonSchemaObject;
69
+ extensionBindings?: ExtensionBindings;
66
70
  turnBudget?: ResolvedTurnBudget;
67
71
  artifacts?: boolean;
68
72
  artifactDir?: ArtifactDirPreference;
@@ -250,6 +254,15 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
250
254
  return { ok: false, code: "missing_agent", message: `Unknown agent: ${input.agent}`, diagnostics };
251
255
  }
252
256
  const agent = resolvedAgent.agent;
257
+ let extensionBindings: ExtensionBindings | undefined;
258
+ try {
259
+ extensionBindings = normalizeExtensionBindings(input.extensionBindings)?.value;
260
+ } catch (error) {
261
+ return { ok: false, code: "invalid_extension_bindings", message: error instanceof Error ? error.message : String(error), diagnostics };
262
+ }
263
+ if (extensionBindings !== undefined && (agent.runner?.type === "external-cli" || agent.runner?.type === "external-job")) {
264
+ return { ok: false, code: "unsupported_mode", message: `extensionBindings is not supported for runner.type='${agent.runner.type}'.`, diagnostics };
265
+ }
253
266
  const context = resolveLaunchContractContext(input, agent);
254
267
  if (context === "fork") {
255
268
  diagnostics.push({ code: "host_required", severity: "host-required", message: "Exact fork session branching and fork-thinking downgrade checks require Pi host session and model-registry snapshots." });
@@ -313,6 +326,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
313
326
  }
314
327
  let toolPlan: PiLaunchToolPlan;
315
328
  const permissionRules = resolvePermissionRules(loadConfig().permissions, agent.permissions);
329
+ const fast = input.fast ?? agent.fast;
316
330
  try {
317
331
  toolPlan = resolvePiLaunchToolPlan({
318
332
  tools: agent.tools,
@@ -322,6 +336,9 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
322
336
  cwd: effectiveCwd,
323
337
  requireReadTool: resolvedSkills.resolved.length > 0,
324
338
  structuredOutput: Boolean(input.outputSchema),
339
+ fast,
340
+ model,
341
+ modelCandidates,
325
342
  capabilityCeiling: effectiveCapabilityCeiling,
326
343
  agentName: agent.name,
327
344
  permissionRules,
@@ -433,6 +450,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
433
450
  definitionDigest,
434
451
  ...(model ? { model } : {}),
435
452
  modelCandidates,
453
+ ...(fast !== undefined ? { fast } : {}),
436
454
  ...(resolveEffectiveThinking(model, effectiveThinkingConfig) ? { thinking: resolveEffectiveThinking(model, effectiveThinkingConfig) } : {}),
437
455
  systemPrompt: effectiveSystemPrompt,
438
456
  systemPromptMode: agent.systemPromptMode,
@@ -445,6 +463,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
445
463
  ...(outputPath ? { outputPath } : {}),
446
464
  outputMode: behavior.outputMode,
447
465
  ...(input.outputSchema ? { structuredOutputSchema: input.outputSchema } : {}),
466
+ ...(extensionBindings ? { extensionBindings } : {}),
448
467
  }),
449
468
  };
450
469
  return { ok: true, contract: { ...contractBase, digest: digestContract(contractBase) } };
@@ -145,6 +145,7 @@ export const ParallelTaskSchema = Type.Object({
145
145
  progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
146
146
  skill: Type.Optional(SkillOverride),
147
147
  model: Type.Optional(Type.String({ description: "Override model for this task" })),
148
+ fast: Type.Optional(Type.Boolean({ description: "Opt into priority service tier for supported native OpenAI-Codex child models. This can increase quota or cost." })),
148
149
  toolBudget: Type.Optional(ToolBudgetOverride),
149
150
  acceptance: Type.Optional(AcceptanceOverride),
150
151
  agentContract: Type.Optional(AgentContractOverride),
@@ -175,6 +176,7 @@ export const DynamicParallelTemplateSchema = Type.Object({
175
176
  progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
176
177
  skill: Type.Optional(SkillOverride),
177
178
  model: Type.Optional(Type.String({ description: "Override model for this task" })),
179
+ fast: Type.Optional(Type.Boolean({ description: "Opt into priority service tier for supported native OpenAI-Codex child models. This can increase quota or cost." })),
178
180
  toolBudget: Type.Optional(ToolBudgetOverride),
179
181
  acceptance: Type.Optional(AcceptanceOverride),
180
182
  agentContract: Type.Optional(AgentContractOverride),
@@ -203,6 +205,7 @@ export const ChainItem = Type.Object({
203
205
  progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
204
206
  skill: Type.Optional(SkillOverride),
205
207
  model: Type.Optional(Type.String({ description: "Override model for this step" })),
208
+ fast: Type.Optional(Type.Boolean({ description: "Opt into priority service tier for supported native OpenAI-Codex child models. This can increase quota or cost." })),
206
209
  toolBudget: Type.Optional(ToolBudgetOverride),
207
210
  acceptance: Type.Optional(AcceptanceOverride),
208
211
  agentContract: Type.Optional(AgentContractOverride),
@@ -255,6 +258,7 @@ const ControlOverrides = Type.Object({
255
258
  const SubagentParamProperties = {
256
259
  agent: Type.Optional(Type.String({ description: "Agent for one-child execution, or target for agent management actions." })),
257
260
  task: Type.Optional(Type.String({ description: "Optional one-child task. Requires agent; cannot combine with action or workflowScript." })),
261
+ extensionBindings: Type.Optional(Type.Unsafe({ type: "object", maxProperties: 16, additionalProperties: true, description: "Namespaced, bounded plain-JSON metadata delivered only to the child runtime. Namespace keys use package.name/1 syntax." })),
258
262
  // Management action (when present, tool operates in management mode)
259
263
  action: Type.Optional(Type.String({ minLength: 1,
260
264
  description: "Optional management/control action. Omit this field for structured single-child or workflowScript execution; use it only for management/control actions."
@@ -343,6 +347,7 @@ const SubagentParamProperties = {
343
347
  outputMode: Type.Optional(OutputModeOverride),
344
348
  skill: Type.Optional(SkillOverride),
345
349
  model: Type.Optional(Type.String({ description: "Default child model override. Full provider/id values are accepted; bare ids resolve from the active registry." })),
350
+ fast: Type.Optional(Type.Boolean({ description: "Opt into priority service tier for supported native OpenAI-Codex child models. Default false. This can increase quota or cost." })),
346
351
  outputSchema: Type.Optional(JsonSchemaObject),
347
352
  agentContract: Type.Optional(AgentContractOverride),
348
353
  acceptance: Type.Optional(AcceptanceOverride),
@@ -74,6 +74,7 @@ import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
74
74
  import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
75
75
  import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
76
76
  import { resolvePermissionRules, type PermissionConfig } from "../shared/permissions.ts";
77
+ import { normalizeExtensionBindings, omitExtensionBindingsEnv, type ExtensionBindings } from "../shared/extension-bindings.ts";
77
78
 
78
79
  const require = createRequire(import.meta.url);
79
80
  const piPackageRoot = resolvePiPackageRoot();
@@ -175,6 +176,7 @@ interface AsyncChainParams {
175
176
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
176
177
  nestedRoute?: NestedRouteInfo;
177
178
  acceptance?: AcceptanceInput;
179
+ fast?: boolean;
178
180
  timeoutMs?: number;
179
181
  turnBudget?: ResolvedTurnBudget;
180
182
  toolBudget?: ResolvedToolBudget;
@@ -224,6 +226,7 @@ interface AsyncSingleParams {
224
226
  structuredOutputSchema?: JsonSchemaObject;
225
227
  modelOverride?: string;
226
228
  modelOverrideFromParent?: boolean;
229
+ fast?: boolean;
227
230
  thinkingOverride?: AgentConfig["thinking"];
228
231
  availableModels?: AvailableModelInfo[];
229
232
  maxSubagentDepth: number;
@@ -265,6 +268,7 @@ interface AsyncSingleParams {
265
268
  requestId: string;
266
269
  requestDigest: string;
267
270
  };
271
+ extensionBindings?: ExtensionBindings;
268
272
  }
269
273
 
270
274
  interface AsyncExecutionResult {
@@ -295,6 +299,7 @@ export interface AsyncRunnerStepBuildParams {
295
299
  asyncDir: string;
296
300
  outputBaseDir?: string;
297
301
  validateOutputBindings?: boolean;
302
+ fast?: boolean;
298
303
  toolBudget?: ResolvedToolBudget;
299
304
  configToolBudget?: ResolvedToolBudget;
300
305
  /** Optional per-call hard toolTimeoutMs override from the subagent invocation. */
@@ -506,7 +511,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
506
511
  const cfgPath = getAsyncConfigPath(suffix);
507
512
  const runnerProcessInstanceId = randomUUID();
508
513
  const launchConfig = { ...cfg, runnerProcessInstanceId };
509
- fs.writeFileSync(cfgPath, JSON.stringify(launchConfig));
514
+ writePrivateAtomicJson(cfgPath, launchConfig);
510
515
  const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
511
516
  const nodeCommand = resolveNodeExecutable();
512
517
  const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; completionOwnerId?: unknown; revivalLease?: unknown };
@@ -538,7 +543,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, onProcessTerminal
538
543
  stdio: ["ignore", stdoutFd ?? "ignore", stderrFd ?? "ignore"],
539
544
  windowsHide: true,
540
545
  env: {
541
- ...process.env,
546
+ ...omitExtensionBindingsEnv(process.env),
542
547
  ...(piPackageRoot ? { [PI_CODING_AGENT_PACKAGE_ROOT_ENV]: piPackageRoot } : {}),
543
548
  },
544
549
  });
@@ -727,6 +732,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
727
732
  ...(s.progress !== undefined ? { progress: s.progress } : {}),
728
733
  ...(stepSkillInput !== undefined ? { skills: stepSkillInput } : {}),
729
734
  ...(s.model !== undefined ? { model: s.model } : {}),
735
+ ...(s.fast !== undefined ? { fast: s.fast } : {}),
730
736
  };
731
737
  };
732
738
  const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, flatIndex?: number, parallelOutputNamespace?: { stepIndex: number; taskIndex?: number }, runFanoutPath?: string) => {
@@ -739,6 +745,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
739
745
  if (s.outputSchema !== undefined) unsupported.push("structured output");
740
746
  if (s.acceptance !== undefined || params.agentContract !== undefined || s.agentContract !== undefined) unsupported.push("acceptance/agent contract");
741
747
  if (s.toolBudget !== undefined || params.toolBudget !== undefined || a.toolBudget !== undefined || params.configToolBudget !== undefined) unsupported.push("tool budget");
748
+ if ((s.fast ?? params.fast ?? a.fast) === true) unsupported.push("fast mode");
742
749
  if (params.contextForAgent?.(s.agent) === "fork") unsupported.push("fork context");
743
750
  if (unsupported.length > 0) throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}' and does not support: ${unsupported.join(", ")}.`);
744
751
  }
@@ -831,6 +838,21 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
831
838
  }
832
839
  const agentContract = s.agentContract ?? params.agentContract;
833
840
  const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
841
+ const modelCandidates = externalRunner ? [] : buildModelCandidates(primaryModel, a.fallbackModels, availableModels, a.modelProvider ?? ctx.currentModelProvider, {
842
+ scope: modelScopes,
843
+ primaryModelFromParent,
844
+ }).flatMap((candidate) => {
845
+ const resolved = applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined);
846
+ return resolved ? [resolved] : [];
847
+ });
848
+ if (!externalRunner) {
849
+ try {
850
+ for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: a.name, runId: id });
851
+ } catch (error) {
852
+ throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
853
+ }
854
+ }
855
+ const fast = s.fast ?? params.fast ?? a.fast;
834
856
  const toolPlan = resolvePiLaunchToolPlan({
835
857
  tools: a.tools,
836
858
  extensions: a.extensions,
@@ -839,6 +861,9 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
839
861
  cwd: stepCwd,
840
862
  requireReadTool: Boolean(resolvedSkills.length),
841
863
  structuredOutput: Boolean(s.outputSchema),
864
+ fast,
865
+ model,
866
+ modelCandidates,
842
867
  capabilityCeiling: params.capabilityCeiling,
843
868
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
844
869
  agentName: a.name,
@@ -862,20 +887,6 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
862
887
  });
863
888
  if (contractError) throw new AsyncStartValidationError(contractError);
864
889
  }
865
- const modelCandidates = externalRunner ? [] : buildModelCandidates(primaryModel, a.fallbackModels, availableModels, a.modelProvider ?? ctx.currentModelProvider, {
866
- scope: modelScopes,
867
- primaryModelFromParent,
868
- }).flatMap((candidate) => {
869
- const resolved = applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined);
870
- return resolved ? [resolved] : [];
871
- });
872
- if (!externalRunner) {
873
- try {
874
- for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: a.name, runId: id });
875
- } catch (error) {
876
- throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
877
- }
878
- }
879
890
  return {
880
891
  parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
881
892
  permissionRules,
@@ -892,6 +903,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
892
903
  structured: Boolean(s.outputSchema),
893
904
  cwd: stepCwd,
894
905
  model,
906
+ ...(fast !== undefined ? { fast } : {}),
895
907
  thinking: resolveEffectiveThinking(model, effectiveThinking),
896
908
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
897
909
  launchResolvedExtensions,
@@ -930,7 +942,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
930
942
  acceptanceRole: a.acceptanceRole,
931
943
  ...(s.gateOn ? { gateOn: s.gateOn } : {}),
932
944
  ...(s.outputSchema ? { structuredOutputSchema: s.outputSchema } : {}),
933
- ...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output")) } : {}),
945
+ ...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output"), { captureAcceptanceReport: s.acceptance !== false }) } : {}),
934
946
  ...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
935
947
  ...(s.worktree ? { worktree: true } : {}),
936
948
  };
@@ -1143,6 +1155,7 @@ export function executeAsyncChain(
1143
1155
  waitToolEnabled: params.waitToolEnabled,
1144
1156
  worktreeBaseDir,
1145
1157
  asyncDir,
1158
+ fast: params.fast,
1146
1159
  toolBudget: params.toolBudget,
1147
1160
  configToolBudget: params.configToolBudget,
1148
1161
  callToolTimeoutMs: params.callToolTimeoutMs,
@@ -1391,6 +1404,12 @@ export function executeAsyncSingle(
1391
1404
  nestedRoute,
1392
1405
  } = params;
1393
1406
  const task = params.task ?? "";
1407
+ let extensionBindings: ExtensionBindings | undefined;
1408
+ try {
1409
+ extensionBindings = normalizeExtensionBindings(params.extensionBindings)?.value;
1410
+ } catch (error) {
1411
+ return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1412
+ }
1394
1413
  const acceptanceErrors = validateAcceptanceInput(params.acceptance);
1395
1414
  if (acceptanceErrors.length > 0) return formatAsyncStartError("single", acceptanceErrors.join(" "));
1396
1415
  const externalRunner = agentConfig.runner?.type === "external-cli" || agentConfig.runner?.type === "external-job";
@@ -1399,6 +1418,7 @@ export function executeAsyncSingle(
1399
1418
  if (externalRunner) {
1400
1419
  const unsupported: string[] = [];
1401
1420
  if (params.modelOverride !== undefined) unsupported.push("model override");
1421
+ if ((params.fast ?? agentConfig.fast) === true) unsupported.push("fast mode");
1402
1422
  if (params.thinkingOverride !== undefined) unsupported.push("thinking override");
1403
1423
  if (params.structuredOutputSchema !== undefined) unsupported.push("structured output");
1404
1424
  if (params.acceptance !== undefined || params.agentContract !== undefined) unsupported.push("acceptance/agent contract");
@@ -1406,6 +1426,7 @@ export function executeAsyncSingle(
1406
1426
  if (params.context === "fork") unsupported.push("fork context");
1407
1427
  if ((params.skills?.length ?? 0) > 0) unsupported.push("skills");
1408
1428
  if (permissionRules) unsupported.push("native Pi child permissions");
1429
+ if (extensionBindings !== undefined) unsupported.push("extension bindings");
1409
1430
  if (unsupported.length > 0) return formatAsyncStartError("single", `Agent '${agentConfig.name}' uses runner.type='${externalRunnerType}' and does not support: ${unsupported.join(", ")}.`);
1410
1431
  }
1411
1432
  const capabilityCeiling = intersectSubagentCapabilityCeilings(params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId), decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]));
@@ -1518,7 +1539,7 @@ export function executeAsyncSingle(
1518
1539
  const initialUsageBudget = usageBudgetState(params.usageBudget, undefined);
1519
1540
  const resolvedSessionDir = params.sessionDir ?? (sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined);
1520
1541
  const structuredOutput = params.structuredOutputSchema
1521
- ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"))
1542
+ ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"), { captureAcceptanceReport: params.acceptance !== false })
1522
1543
  : undefined;
1523
1544
  const modelCandidates = externalRunner
1524
1545
  ? []
@@ -1546,6 +1567,9 @@ export function executeAsyncSingle(
1546
1567
  cwd: runnerCwd,
1547
1568
  requireReadTool: Boolean(resolvedSkills.length),
1548
1569
  structuredOutput: Boolean(params.structuredOutputSchema),
1570
+ fast: params.fast ?? agentConfig.fast,
1571
+ model,
1572
+ modelCandidates,
1549
1573
  capabilityCeiling,
1550
1574
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1551
1575
  agentName: agentConfig.name,
@@ -1571,6 +1595,7 @@ export function executeAsyncSingle(
1571
1595
  task,
1572
1596
  ...(model ? { model } : {}),
1573
1597
  modelCandidates,
1598
+ ...((params.fast ?? agentConfig.fast) !== undefined ? { fast: params.fast ?? agentConfig.fast } : {}),
1574
1599
  ...(resolveEffectiveThinking(model, effectiveThinking) ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
1575
1600
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
1576
1601
  systemPrompt: effectiveSystemPrompt,
@@ -1584,6 +1609,7 @@ export function executeAsyncSingle(
1584
1609
  ...(outputPath ? { outputPath } : {}),
1585
1610
  outputMode,
1586
1611
  ...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
1612
+ ...(extensionBindings ? { extensionBindings } : {}),
1587
1613
  });
1588
1614
  const resolvedAcceptance = resolveEffectiveAcceptance({
1589
1615
  explicit: params.acceptance,
@@ -1598,6 +1624,7 @@ export function executeAsyncSingle(
1598
1624
  const recoveryDescriptor: SteeringRecoveryDescriptor = {
1599
1625
  version: 1,
1600
1626
  launchContractDigest,
1627
+ ...(extensionBindings ? { extensionBindings } : {}),
1601
1628
  runFanoutBudget,
1602
1629
  sourceRunId: id,
1603
1630
  ...(params.agentContract ? { agentContract: params.agentContract } : {}),
@@ -1606,6 +1633,7 @@ export function executeAsyncSingle(
1606
1633
  ...(sessionFile ? { sessionFile } : {}),
1607
1634
  cwd: runnerCwd,
1608
1635
  ...(model ? { model } : {}),
1636
+ ...(params.fast ?? recoveryAgentConfig.fast ? { fast: params.fast ?? recoveryAgentConfig.fast } : {}),
1609
1637
  ...(recoveryAgentConfig.modelProvider ? { modelProvider: recoveryAgentConfig.modelProvider } : {}),
1610
1638
  ...(params.modelOverrideFromParent ? { modelOverrideFromParent: true } : {}),
1611
1639
  ...(recoveryAgentConfig.fallbackModels ? { fallbackModels: [...recoveryAgentConfig.fallbackModels] } : {}),
@@ -1666,6 +1694,7 @@ export function executeAsyncSingle(
1666
1694
  ...(params.context ? { context: params.context } : {}),
1667
1695
  cwd: runnerCwd,
1668
1696
  model,
1697
+ ...(params.fast ?? agentConfig.fast ? { fast: params.fast ?? agentConfig.fast } : {}),
1669
1698
  thinking: resolveEffectiveThinking(model, effectiveThinking),
1670
1699
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
1671
1700
  modelCandidates,
@@ -1690,6 +1719,7 @@ export function executeAsyncSingle(
1690
1719
  definitionDigest: agentDefinitionDigest(agentConfig),
1691
1720
  launchBindingTask: task,
1692
1721
  launchContractDigest,
1722
+ ...(extensionBindings ? { extensionBindings } : {}),
1693
1723
  launchResolvedExtensions,
1694
1724
  effectiveAcceptance: resolvedAcceptance,
1695
1725
  ...(structuredOutput ? { structuredOutput } : {}),
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { DIRS, type AcceptanceInput, type AsyncStatus, type ResolvedTurnBudget, type SteeringRecoveryDescriptor, type SubagentRunMode } from "../../shared/types.ts";
4
4
  import type { AgentConfig } from "../../agents/agents.ts";
5
+ import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
5
6
  import { validateAcceptanceInput } from "../shared/acceptance.ts";
6
7
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
7
8
  import { intersectSubagentCapabilityCeilings, parseSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
@@ -328,6 +329,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
328
329
  "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
329
330
  "artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
330
331
  "launchResolvedExtensions", "runFanoutBudget",
332
+ "extensionBindings",
331
333
  ]);
332
334
  for (const field of Object.keys(parsed)) {
333
335
  if (!allowedFields.has(field)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': unknown field '${field}'.`);
@@ -344,6 +346,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
344
346
  }
345
347
  if (parsed.capabilityCeiling !== undefined) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
346
348
  if (parsed.thinkingCeiling !== undefined) parsed.thinkingCeiling = parseThinkingLevel(parsed.thinkingCeiling, `async recovery descriptor '${descriptorPath}' thinkingCeiling`);
349
+ if (parsed.extensionBindings !== undefined) parsed.extensionBindings = normalizeExtensionBindings(parsed.extensionBindings)!.value;
347
350
  if (parsed.agentContract !== undefined) {
348
351
  if (!parsed.agentContract || typeof parsed.agentContract !== "object" || Array.isArray(parsed.agentContract)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be an object.`);
349
352
  const contract = parsed.agentContract as Record<string, unknown>;