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
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { AcceptanceInput, AcceptanceRole, AgentRunnerConfig, OutputMode, ToolBudgetConfig, TurnBudgetConfig } from "../shared/types.ts";
3
+ import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL, isCodeOwnedExternalCliAdapterId, parseExternalCliCapabilityNarrowing, validateCodeOwnedProfileRunner } from "../runs/shared/external-cli-contract.ts";
3
4
  import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
4
5
  import { validatePermissionRules, type PermissionRules } from "../runs/shared/permissions.ts";
5
6
  import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
@@ -169,12 +170,15 @@ function validateRunner(value: unknown): AgentRunnerConfig | undefined {
169
170
  if (runner.type !== "external-cli") throw new Error("Runtime agent definition runner.type must be 'pi', 'external-cli', or 'external-job'.");
170
171
  if (typeof runner.command !== "string" || !runner.command.trim()) throw new Error("Runtime agent definition external-cli runner requires a non-empty command string.");
171
172
  if (runner.args !== undefined && (!Array.isArray(runner.args) || runner.args.some((arg) => typeof arg !== "string"))) throw new Error("Runtime agent definition external-cli runner args must be an array of strings.");
173
+ if (runner.adapter !== undefined && !isCodeOwnedExternalCliAdapterId(runner.adapter)) throw new Error(`Runtime agent definition external-cli runner adapter must be ${CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL}.`);
174
+ if (runner.adapter !== undefined && Array.isArray(runner.args) && runner.args.length > 0) throw new Error(`Runtime agent definition ${runner.adapter} adapter owns its argv; runner args are not supported.`);
172
175
  if (runner.promptDelivery !== undefined && runner.promptDelivery !== "stdin") throw new Error("Runtime agent definition external-cli runner promptDelivery must be 'stdin'.");
173
- const supported = new Set(["type", "command", "args", "promptDelivery"]);
176
+ const capabilities = parseExternalCliCapabilityNarrowing(runner.capabilities, "Runtime agent definition external-cli runner capabilities");
177
+ const supported = new Set(["type", "adapter", "command", "args", "promptDelivery", "capabilities"]);
174
178
  const unknown = Object.keys(runner).filter((key) => !supported.has(key));
175
179
  if (unknown.length > 0) throw new Error(`Runtime agent definition external-cli runner has unsupported fields: ${unknown.join(", ")}.`);
176
180
  const args = runner.args as string[] | undefined;
177
- return { type: "external-cli", command: runner.command.trim(), ...(args?.length ? { args } : {}), ...(runner.promptDelivery ? { promptDelivery: "stdin" as const } : {}) };
181
+ return { type: "external-cli", ...(isCodeOwnedExternalCliAdapterId(runner.adapter) ? { adapter: runner.adapter } : {}), command: runner.command.trim(), ...(args?.length ? { args } : {}), ...(runner.promptDelivery ? { promptDelivery: "stdin" as const } : {}), ...(capabilities ? { capabilities } : {}) };
178
182
  }
179
183
 
180
184
  function validateTurnBudget(value: unknown): TurnBudgetConfig | undefined {
@@ -364,6 +368,8 @@ export function registerRuntimeAgent(input: RegisterRuntimeAgentInput): RuntimeA
364
368
  const name = validateString(input.name, "Runtime agent name", MAX_AGENT_NAME_LENGTH);
365
369
  const definition = validateDefinition(input.definition);
366
370
  const agent = toAgentConfig(name, definition);
371
+ const profileError = validateCodeOwnedProfileRunner(agent);
372
+ if (profileError) throw new Error(profileError);
367
373
  assertNoBuiltinCollision(agent);
368
374
  const current = registry();
369
375
  const records = current.byPi.get(pi) ?? [];
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { discoverAgents, discoverAgentsAll, findBlockingAgentDiagnostic, resolveAgentName, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
4
+ import { discoverAgents, discoverAgentsAll, findBlockingAgentDiagnostic, formatUnknownAgentError, resolveAgentName, unknownAgentDiagnosticContext, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
5
5
  import { resolveExecutionAgentScope } from "../agents/agent-scope.ts";
6
6
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../agents/skills.ts";
7
7
  import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
@@ -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;
@@ -247,9 +251,18 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
247
251
  return { ok: false, code: "ambiguous_agent", message: resolvedAgent.error, diagnostics };
248
252
  }
249
253
  if (!resolvedAgent.agent) {
250
- return { ok: false, code: "missing_agent", message: `Unknown agent: ${input.agent}`, diagnostics };
254
+ return { ok: false, code: "missing_agent", message: formatUnknownAgentError(input.agent, unknownAgentDiagnosticContext(discovered)), 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) } };
@@ -6,7 +6,9 @@ import { FLEET_KEYBINDING_ACTIONS, type ArtifactDirPreference, type ExtensionCon
6
6
  import { validateMissionStoreConfig } from "../missions/store.ts";
7
7
  import { validateAuthorityPolicy } from "../policy/authority.ts";
8
8
  import { getAgentDir } from "../shared/utils.ts";
9
+ import { DEFAULT_MODEL_EXCLUSION_TTL_MS, MAX_MODEL_EXCLUSION_TTL_MS, setDefaultTTL } from "../runs/shared/model-exclusions.ts";
9
10
  import { validatePermissionConfig } from "../runs/shared/permissions.ts";
11
+ import { MAX_ABANDONED_SLOT_RELEASE_AFTER_MS, MIN_ABANDONED_SLOT_RELEASE_AFTER_MS } from "../runs/background/active-async-capacity.ts";
10
12
 
11
13
  const ARTIFACT_DIR_PREFERENCES = new Set<ArtifactDirPreference>(["project", "session", "temp"]);
12
14
  const FLEET_KEYBINDING_ACTION_SET = new Set<string>(FLEET_KEYBINDING_ACTIONS);
@@ -16,6 +18,23 @@ const BASE_KEY_IDS = new Set([
16
18
  ...Object.values(Key).flatMap((value) => typeof value === "string" ? [value.toLowerCase()] : []),
17
19
  ]);
18
20
 
21
+ class PrunedForkConfigError extends Error {}
22
+
23
+ function validateForkContextConfig(value: unknown): void {
24
+ if (value === undefined) return;
25
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.forkContext must be a JSON object");
26
+ const config = value as Record<string, unknown>;
27
+ if (config.mode !== undefined && config.mode !== "full" && config.mode !== "pruned") {
28
+ throw new Error('config.forkContext.mode must be "full" or "pruned"');
29
+ }
30
+ if (config.model !== undefined && (typeof config.model !== "string" || !config.model.trim())) {
31
+ throw new PrunedForkConfigError("config.forkContext.model must be a non-empty string");
32
+ }
33
+ if (config.mode === "pruned" && config.model === undefined) {
34
+ throw new PrunedForkConfigError('config.forkContext.model is required when config.forkContext.mode is "pruned"');
35
+ }
36
+ }
37
+
19
38
  function isValidKeyId(value: string): boolean {
20
39
  if (value !== value.trim()) return false;
21
40
  const parts = value.toLowerCase().split("+");
@@ -62,6 +81,31 @@ function validateArtifactConfig(value: unknown): void {
62
81
  }
63
82
  }
64
83
 
84
+ function validateCapacityConfig(value: unknown): void {
85
+ if (value === undefined) return;
86
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.capacity must be a JSON object");
87
+ const abandonedSlotReleaseAfterMs = (value as Record<string, unknown>).abandonedSlotReleaseAfterMs;
88
+ if (abandonedSlotReleaseAfterMs !== undefined
89
+ && abandonedSlotReleaseAfterMs !== false
90
+ && (typeof abandonedSlotReleaseAfterMs !== "number"
91
+ || !Number.isInteger(abandonedSlotReleaseAfterMs)
92
+ || abandonedSlotReleaseAfterMs < MIN_ABANDONED_SLOT_RELEASE_AFTER_MS
93
+ || abandonedSlotReleaseAfterMs > MAX_ABANDONED_SLOT_RELEASE_AFTER_MS)) {
94
+ throw new Error(`config.capacity.abandonedSlotReleaseAfterMs must be false or an integer from ${MIN_ABANDONED_SLOT_RELEASE_AFTER_MS} to ${MAX_ABANDONED_SLOT_RELEASE_AFTER_MS}`);
95
+ }
96
+ }
97
+
98
+ /** Validate the user-controlled TTL policy before it reaches the exclusion store. */
99
+ // TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
100
+ function validateModelExclusionsConfig(value: unknown): void {
101
+ if (value === undefined) return;
102
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.modelExclusions must be a JSON object");
103
+ const defaultTtlMs = (value as Record<string, unknown>).defaultTtlMs;
104
+ if (defaultTtlMs !== undefined && (typeof defaultTtlMs !== "number" || !Number.isFinite(defaultTtlMs) || defaultTtlMs <= 0 || defaultTtlMs > MAX_MODEL_EXCLUSION_TTL_MS)) {
105
+ throw new Error(`config.modelExclusions.defaultTtlMs must be a finite positive number no greater than ${MAX_MODEL_EXCLUSION_TTL_MS}`);
106
+ }
107
+ }
108
+
65
109
  function validateOrcaProgressTabsConfig(value: unknown): void {
66
110
  if (value === undefined) return;
67
111
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.orcaProgressTabs must be a JSON object");
@@ -97,6 +141,7 @@ function validateConfig(config: Record<string, unknown>): void {
97
141
  if (config.defaultSubagentContext !== undefined && config.defaultSubagentContext !== "fresh" && config.defaultSubagentContext !== "fork") {
98
142
  throw new Error('config.defaultSubagentContext must be "fresh" or "fork"');
99
143
  }
144
+ validateForkContextConfig(config.forkContext);
100
145
  if (config.foregroundDetachShortcut !== undefined
101
146
  && (typeof config.foregroundDetachShortcut !== "string" || !isValidKeyId(config.foregroundDetachShortcut))) {
102
147
  throw new Error("config.foregroundDetachShortcut must be a valid keybinding string such as \"ctrl+b\"");
@@ -119,6 +164,8 @@ function validateConfig(config: Record<string, unknown>): void {
119
164
  validateScheduledRunsConfig(config.scheduledRuns);
120
165
  validateFleetKeybindingsConfig(config.fleetKeybindings);
121
166
  validateArtifactConfig(config.artifactConfig);
167
+ validateCapacityConfig(config.capacity);
168
+ validateModelExclusionsConfig(config.modelExclusions);
122
169
  validateMainWindowRendererConfig(config.mainWindowRenderer);
123
170
  validateOrcaProgressTabsConfig(config.orcaProgressTabs);
124
171
  }
@@ -150,6 +197,28 @@ export function updateConfig(updater: (config: ExtensionConfig) => ExtensionConf
150
197
  return next;
151
198
  }
152
199
 
200
+ /**
201
+ * Resolve the default TTL that the process-wide exclusion store should use.
202
+ *
203
+ * @param config Extension configuration after validation.
204
+ * @returns The configured TTL in milliseconds, or the built-in 24-hour default.
205
+ */
206
+ // TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
207
+ export function resolveModelExclusionTTL(config: Pick<ExtensionConfig, "modelExclusions">): number {
208
+ return config.modelExclusions?.defaultTtlMs ?? DEFAULT_MODEL_EXCLUSION_TTL_MS;
209
+ }
210
+
211
+ /**
212
+ * Apply the configured exclusion policy to the process-wide model store.
213
+ *
214
+ * @param config Extension configuration after validation.
215
+ * @returns Nothing.
216
+ */
217
+ // TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
218
+ export function applyModelExclusionsConfig(config: Pick<ExtensionConfig, "modelExclusions">): void {
219
+ setDefaultTTL(resolveModelExclusionTTL(config), { shortenExisting: config.modelExclusions?.defaultTtlMs !== undefined });
220
+ }
221
+
153
222
  export function resolveAsyncByDefault(config: Pick<ExtensionConfig, "asyncByDefault">): boolean {
154
223
  return config.asyncByDefault !== false;
155
224
  }
@@ -159,6 +228,7 @@ export function loadConfig(): ExtensionConfig {
159
228
  try {
160
229
  return readConfigForUpdate(configPath);
161
230
  } catch (error) {
231
+ if (error instanceof PrunedForkConfigError) throw error;
162
232
  console.error(`Failed to load subagent config from '${configPath}':`, error);
163
233
  }
164
234
  return {};
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import { discoverAgentsAll, type AgentSource } from "../agents/agents.ts";
4
4
  import { isAsyncAvailable } from "../runs/background/async-execution.ts";
5
5
  import { formatSpawnBudgetSummary, getSpawnBudgetSnapshot } from "../runs/shared/spawn-budget.ts";
6
- import { getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
6
+ import { getActiveAsyncCapacitySnapshot, resolveAbandonedSlotReleaseAfterMs, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
7
7
  import { decodeRunFanoutBudgetDescriptor, formatRunFanoutBudget, getRunFanoutBudgetSnapshot, RUN_FANOUT_BUDGET_ENV } from "../runs/shared/run-fanout-budget.ts";
8
8
  import { diagnoseIntercomBridge, type IntercomBridgeDiagnostic } from "../intercom/intercom-bridge.ts";
9
9
  import { discoverAvailableSkills, type SkillSource } from "../agents/skills.ts";
@@ -197,13 +197,13 @@ function formatActiveAsyncCapacitySection(input: DoctorReportInput): string[] {
197
197
  const limit = resolveMaxActiveAsyncRunsPerSession(input.config.maxActiveAsyncRunsPerSession);
198
198
  const sessionId = input.currentSessionId ?? input.state.currentSessionId;
199
199
  const snapshot = sessionId
200
- ? getActiveAsyncCapacitySnapshot(sessionId, limit, { liveWorkflowRunIds: new Set(input.state.workflowControllers?.keys() ?? []) })
200
+ ? getActiveAsyncCapacitySnapshot(sessionId, limit, { liveWorkflowRunIds: new Set(input.state.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(input.config.capacity?.abandonedSlotReleaseAfterMs) })
201
201
  : { used: 0, limit: limit ?? 0 };
202
202
  input.state.activeAsyncCapacity = snapshot;
203
203
  return [
204
204
  `- usage: ${snapshot.used}/${snapshot.limit || "unlimited"} used`,
205
205
  "- scope: top-level async runs in the current parent session; foreground and nested workflow children are not charged again",
206
- "- release: terminal logical state plus verified process exit; missing or unknown cleanup proof retains capacity",
206
+ "- release: terminal state plus matching observed process-terminal proof, or abandoned-timeout for failed runs with a dead runner PID and stale activity when enabled; false keeps unknown-proof slots",
207
207
  ];
208
208
  }
209
209
 
@@ -9,7 +9,7 @@
9
9
  * Toggle: async parameter (default: true; set asyncByDefault:false in config.json to opt out)
10
10
  *
11
11
  * Config file: ~/.pi/agent/extensions/subagent/config.json
12
- * { "asyncByDefault": true, "defaultSubagentContext": "fork", "forceTopLevelAsync": true, "maxSubagentDepth": 1, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
12
+ * { "asyncByDefault": true, "defaultSubagentContext": "fork", "forkContext": { "mode": "pruned", "model": "provider/model" }, "forceTopLevelAsync": true, "maxSubagentDepth": 1, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
13
13
  */
14
14
 
15
15
  import { randomUUID } from "node:crypto";
@@ -24,6 +24,7 @@ import { clearRuntimeAgentsForPi, listRuntimeAgentConfigs, mergeRuntimeAgents }
24
24
  import { ensureAccessibleDir } from "../shared/accessible-dir.ts";
25
25
  import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
26
26
  import { resolveCurrentSessionId } from "../shared/session-identity.ts";
27
+ import { getAgentDir } from "../shared/utils.ts";
27
28
  import { currentCompletionOwnerId } from "../shared/completion-owner.ts";
28
29
  import { cleanupOldChainDirs } from "../shared/settings.ts";
29
30
  import { clearLegacyResultAnimationTimer, renderSubagentResult, renderSubagentSummary } from "../tui/render.ts";
@@ -32,7 +33,7 @@ import { SubagentFleetStatus, resolveFleetViewPlacement } from "../tui/fleet-sta
32
33
  import { createSubagentParamsSchema } from "./schemas.ts";
33
34
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
34
35
  import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
35
- import { getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
36
+ import { getActiveAsyncCapacitySnapshot, resolveAbandonedSlotReleaseAfterMs, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
36
37
  import { cleanupResultIndexes, missionObserverResultCandidateFiles } from "../runs/background/result-files.ts";
37
38
  import { ASYNC_RETENTION_DELAY_MS, cleanupAsyncRetention } from "../runs/background/async-retention.ts";
38
39
  import { createResultWatcher } from "../runs/background/result-watcher.ts";
@@ -56,7 +57,7 @@ import { formatSteeringNotice, handleSubagentSteeringNotice, SUBAGENT_STEERING_M
56
57
  import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/pi-args.ts";
57
58
  import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
58
59
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
59
- import { loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
60
+ import { applyModelExclusionsConfig, loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
60
61
  import { buildSubagentToolDescription, buildSubagentToolPromptMetadata } from "./tool-description.ts";
61
62
  import { finalizeToolResult } from "./tool-result.ts";
62
63
  import { collectGoalContinuationNotices } from "../missions/goal-driver.ts";
@@ -382,9 +383,13 @@ export function projectActiveHerdrRuns(state: SubagentState): HerdrStatusRun[] {
382
383
  .filter((job) => active(job.status))
383
384
  .map((job) => {
384
385
  const children = job.mode === "workflow" ? foregroundChildrenByWorkflow.get(job.asyncId) : undefined;
386
+ const currentStep = job.steps?.find((step) => step.status === "running")
387
+ ?? (job.currentStep !== undefined ? job.steps?.[job.currentStep] : undefined)
388
+ ?? job.steps?.find((step) => step.status === "pending");
385
389
  return {
386
390
  id: job.asyncId,
387
391
  agents: children?.length ? children.map((child) => child.agent) : job.agents,
392
+ ...(currentStep?.label ? { taskLabel: currentStep.label } : {}),
388
393
  needsAttention: job.activityState === "needs_attention" || children?.some((child) => child.needsAttention),
389
394
  };
390
395
  });
@@ -401,6 +406,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
401
406
  cleanupOldChainDirs();
402
407
 
403
408
  const config = loadConfig();
409
+ // Apply the process-wide exclusion TTL before any child launch can record a model failure.
410
+ applyModelExclusionsConfig(config);
404
411
  const waitToolConfig = resolveWaitToolConfig(config.waitTool);
405
412
  const asyncByDefault = resolveAsyncByDefault(config);
406
413
  const fleetViewEnabled = config.fleetView !== false;
@@ -695,6 +702,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
695
702
  0,
696
703
  0,
697
704
  );
705
+ if (args.workflowScriptPath)
706
+ return new Text(
707
+ `${title}${gap}${theme.fg("accent", args.workflowScriptPath)}${args.async === true ? `${gap}${theme.fg("warning", "[async]")}` : ""}`,
708
+ 0,
709
+ 0,
710
+ );
698
711
  const asyncLabel = args.async === true ? `${gap}${theme.fg("warning", "[async]")}` : "";
699
712
  return new Text(
700
713
  `${title}${gap}${theme.fg("accent", args.agent || "?")}${asyncLabel}`,
@@ -836,7 +849,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
836
849
  state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(
837
850
  state.currentSessionId,
838
851
  resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession),
839
- { liveWorkflowRunIds: new Set(state.workflowControllers?.keys() ?? []) },
852
+ { liveWorkflowRunIds: new Set(state.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(config.capacity?.abandonedSlotReleaseAfterMs) },
840
853
  );
841
854
  };
842
855
 
@@ -846,6 +859,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
846
859
  goalTurnId = 0;
847
860
  state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
848
861
  state.parentSessionFile = ctx.sessionManager.getSessionFile();
862
+ state.trustedSessionFileRoot = state.parentSessionFile ? path.join(getAgentDir(), "sessions") : undefined;
849
863
  state.trustedSessionRoots = [...new Set([
850
864
  ...(config.defaultSessionDir ? [path.resolve(expandTilde(config.defaultSessionDir))] : []),
851
865
  ...(state.parentSessionFile ? [getSubagentSessionRoot(state.parentSessionFile)] : []),
@@ -11,6 +11,7 @@ export interface PublicSubagentExecutionParams {
11
11
  chainName?: unknown;
12
12
  config?: unknown;
13
13
  workflowScript?: unknown;
14
+ workflowScriptPath?: unknown;
14
15
  isolation?: unknown;
15
16
  worktree?: unknown;
16
17
  async?: unknown;
@@ -38,22 +39,28 @@ export type PublicSubagentExecutionNormalization<T> =
38
39
  * Internal runs.run children and structured owned delegation bypass this boundary.
39
40
  */
40
41
  export function normalizePublicSubagentExecution<T extends PublicSubagentExecutionParams>(params: T, options: { asyncByDefault?: boolean } = {}): PublicSubagentExecutionNormalization<T> {
42
+ if (params.workflowScript !== undefined && params.workflowScriptPath !== undefined) {
43
+ return { ok: false, error: "workflowScript and workflowScriptPath are mutually exclusive.", mode: "workflow" };
44
+ }
45
+ const hasWorkflowInput = params.workflowScript !== undefined || params.workflowScriptPath !== undefined;
46
+ const hasValidWorkflowInput = (typeof params.workflowScript === "string" && Boolean(params.workflowScript.trim()))
47
+ || (typeof params.workflowScriptPath === "string" && Boolean(params.workflowScriptPath.trim()));
41
48
  if (params.isolation !== undefined) {
42
49
  if (params.isolation !== "none" && params.isolation !== "worktree") {
43
- return { ok: false, error: "isolation must be 'none' or 'worktree'.", mode: params.workflowScript !== undefined ? "workflow" : "management" };
50
+ return { ok: false, error: "isolation must be 'none' or 'worktree'.", mode: hasWorkflowInput ? "workflow" : "management" };
44
51
  }
45
52
  const isolationWorktree = params.isolation === "worktree";
46
53
  if (params.worktree !== undefined && params.worktree !== isolationWorktree) {
47
- return { ok: false, error: `isolation '${params.isolation}' conflicts with worktree: ${String(params.worktree)}.`, mode: params.workflowScript !== undefined ? "workflow" : "management" };
54
+ return { ok: false, error: `isolation '${params.isolation}' conflicts with worktree: ${String(params.worktree)}.`, mode: hasWorkflowInput ? "workflow" : "management" };
48
55
  }
49
56
  const { isolation: _isolation, ...normalizedParams } = params;
50
57
  params = { ...normalizedParams, worktree: isolationWorktree } as T;
51
58
  }
52
59
  if (params.runFanoutBudget !== undefined || params.runFanoutAdmitted !== undefined) {
53
- return { ok: false, error: "Public execution does not accept internal run fan-out fields.", mode: params.workflowScript !== undefined ? "workflow" : "management" };
60
+ return { ok: false, error: "Public execution does not accept internal run fan-out fields.", mode: hasWorkflowInput ? "workflow" : "management" };
54
61
  }
55
62
  if (params.workflowParentRunId !== undefined || params.workflowKey !== undefined || params.workflowChildAsyncId !== undefined || params.workflowAwaitAsync !== undefined || params.workflowParentDeadlineAt !== undefined || params.suppressRoutineResultIntercom !== undefined) {
56
- return { ok: false, error: "Public execution does not accept internal workflow child fields.", mode: params.workflowScript !== undefined ? "workflow" : "management" };
63
+ return { ok: false, error: "Public execution does not accept internal workflow child fields.", mode: hasWorkflowInput ? "workflow" : "management" };
57
64
  }
58
65
  const action = params.action;
59
66
  if (action !== undefined && (typeof action !== "string" || !action.trim())) {
@@ -90,17 +97,26 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
90
97
  if (legacyAction === "parallel" || legacyAction === "tasks" || legacyAction === "chain") {
91
98
  return { ok: false, error: "Legacy top-level chain and parallel inputs were removed; use workflowScript.", mode: "workflow" };
92
99
  }
100
+ if (normalizedAction === "validate") {
101
+ if (params.agent !== undefined || params.task !== undefined || params.step !== undefined) {
102
+ return { ok: false, error: "validate requires workflowScript or workflowScriptPath and does not accept direct agent, task, or step execution fields.", mode: "management" };
103
+ }
104
+ if (!hasValidWorkflowInput) {
105
+ return { ok: false, error: "validate requires a non-empty workflowScript or workflowScriptPath.", mode: "management" };
106
+ }
107
+ return { ok: true, params: { ...params, action: normalizedAction } };
108
+ }
93
109
  if (normalizedAction === "schedule.create") {
94
110
  if (params.agent !== undefined || params.task !== undefined || params.step !== undefined) {
95
- return { ok: false, error: "schedule.create requires workflowScript and does not accept direct agent, task, or step execution fields.", mode: "management" };
111
+ return { ok: false, error: "schedule.create requires workflowScript or workflowScriptPath and does not accept direct agent, task, or step execution fields.", mode: "management" };
96
112
  }
97
- if (typeof params.workflowScript !== "string" || !params.workflowScript.trim()) {
98
- return { ok: false, error: "schedule.create requires a non-empty workflowScript.", mode: "management" };
113
+ if (!hasValidWorkflowInput) {
114
+ return { ok: false, error: "schedule.create requires a non-empty workflowScript or workflowScriptPath.", mode: "management" };
99
115
  }
100
116
  return { ok: true, params: { ...params, action: normalizedAction } };
101
117
  }
102
- if (params.workflowScript !== undefined) {
103
- return { ok: false, error: "workflowScript execution must omit action; only schedule.create accepts action with workflowScript.", mode: "management" };
118
+ if (hasWorkflowInput) {
119
+ return { ok: false, error: "Workflow execution must omit action; only validate and schedule.create accept action with workflowScript or workflowScriptPath.", mode: "management" };
104
120
  }
105
121
  if (params.task !== undefined) {
106
122
  return { ok: false, error: "Structured single-child task cannot be combined with a management/control action.", mode: "management" };
@@ -110,8 +126,8 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
110
126
  if (params.step !== undefined) {
111
127
  return { ok: false, error: "step is not a public execution field; use workflowScript for orchestration.", mode: "workflow" };
112
128
  }
113
- if (params.workflowScript !== undefined && (params.agent !== undefined || params.task !== undefined)) {
114
- return { ok: false, error: "Structured single-child execution cannot be combined with workflowScript.", mode: "workflow" };
129
+ if (hasWorkflowInput && (params.agent !== undefined || params.task !== undefined)) {
130
+ return { ok: false, error: "Structured single-child execution cannot be combined with workflowScript or workflowScriptPath.", mode: "workflow" };
115
131
  }
116
132
  if (params.agent !== undefined || params.task !== undefined) {
117
133
  if (typeof params.agent !== "string" || !params.agent.trim()) {
@@ -129,8 +145,8 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
129
145
  } as T,
130
146
  };
131
147
  }
132
- if (typeof params.workflowScript !== "string" || !params.workflowScript.trim()) {
133
- return { ok: false, error: "Execution requires either { agent, task? } for one child or a non-empty workflowScript for orchestration.", mode: "workflow" };
148
+ if (!hasValidWorkflowInput) {
149
+ return { ok: false, error: "Execution requires either { agent, task? } for one child or a non-empty workflowScript or workflowScriptPath for orchestration.", mode: "workflow" };
134
150
  }
135
151
  return { ok: true, params };
136
152
  }
@@ -11,6 +11,7 @@ import {
11
11
  type AsyncJobStep,
12
12
  type Details,
13
13
  type SubagentState,
14
+ type TokenUsage,
14
15
  DIRS,
15
16
  SUBAGENT_ASYNC_COMPLETE_EVENT,
16
17
  SUBAGENT_CHILD_STATUS_EVENT,
@@ -97,7 +98,7 @@ export interface SubagentRpcFleetEntry {
97
98
  model?: string;
98
99
  effort?: string;
99
100
  startedAt: number;
100
- tokens: { input: number; output: number; total: number };
101
+ tokens: TokenUsage;
101
102
  goal?: string;
102
103
  }
103
104
 
@@ -122,7 +123,7 @@ function displayText(value: unknown, maxLength: number): string | undefined {
122
123
  return normalized ? truncateDisplayText(normalized, maxLength) : undefined;
123
124
  }
124
125
 
125
- function publicTokens(value: unknown): { input: number; output: number; total: number } {
126
+ function publicTokens(value: unknown): TokenUsage {
126
127
  const record = isRecord(value) ? value : {};
127
128
  const count = (field: "input" | "output" | "total") => {
128
129
  const raw = record[field];
@@ -133,7 +134,21 @@ function publicTokens(value: unknown): { input: number; output: number; total: n
133
134
  const input = count("input");
134
135
  const output = count("output");
135
136
  const sum = Math.min(Number.MAX_SAFE_INTEGER, input + output);
136
- return { input, output, total: Math.max(sum, count("total")) };
137
+ const optionalCount = (field: "window" | "windowPeak") => {
138
+ const raw = record[field];
139
+ return typeof raw === "number" && Number.isFinite(raw) && raw >= 0
140
+ ? Math.min(Number.MAX_SAFE_INTEGER, Math.floor(raw))
141
+ : undefined;
142
+ };
143
+ const window = optionalCount("window");
144
+ const windowPeak = optionalCount("windowPeak");
145
+ return {
146
+ input,
147
+ output,
148
+ total: Math.max(sum, count("total")),
149
+ ...(window !== undefined ? { window } : {}),
150
+ ...(windowPeak !== undefined ? { windowPeak } : {}),
151
+ };
137
152
  }
138
153
 
139
154
  function activeState(value: unknown): boolean {
@@ -188,7 +203,7 @@ function buildFleetStatus(
188
203
  model: child.model,
189
204
  effort: child.thinking,
190
205
  startedAt: child.startedAt,
191
- tokens: { input: child.inputTokens ?? 0, output: child.outputTokens ?? 0, total: child.tokens ?? 0 },
206
+ tokens: { input: child.inputTokens ?? 0, output: child.outputTokens ?? 0, total: child.tokens ?? 0, ...(child.window !== undefined ? { window: child.window } : {}), ...(child.windowPeak !== undefined ? { windowPeak: child.windowPeak } : {}) },
192
207
  });
193
208
  } else {
194
209
  addCandidate({
@@ -197,7 +212,7 @@ function buildFleetStatus(
197
212
  model: control.model,
198
213
  effort: control.thinking,
199
214
  startedAt: control.startedAt,
200
- tokens: { input: control.inputTokens ?? 0, output: control.outputTokens ?? 0, total: control.tokens ?? 0 },
215
+ tokens: { input: control.inputTokens ?? 0, output: control.outputTokens ?? 0, total: control.tokens ?? 0, ...(control.window !== undefined ? { window: control.window } : {}), ...(control.windowPeak !== undefined ? { windowPeak: control.windowPeak } : {}) },
201
216
  });
202
217
  }
203
218
  }
@@ -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),
@@ -254,10 +257,11 @@ const ControlOverrides = Type.Object({
254
257
 
255
258
  const SubagentParamProperties = {
256
259
  agent: Type.Optional(Type.String({ description: "Agent for one-child execution, or target for agent management actions." })),
257
- task: Type.Optional(Type.String({ description: "Optional one-child task. Requires agent; cannot combine with action or workflowScript." })),
260
+ task: Type.Optional(Type.String({ description: "Optional one-child task. Requires agent; cannot combine with action, workflowScript, or workflowScriptPath." })),
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
- description: "Optional management/control action. Omit this field for structured single-child or workflowScript execution; use it only for management/control actions."
264
+ description: "Optional management/control action. Use action='validate' with workflowScript or workflowScriptPath for offline checks. Omit this field for structured single-child or workflow execution; otherwise, use it only for management/control actions."
261
265
  })),
262
266
  name: Type.Optional(Type.String({ description: "Human-readable name for action='schedule.create'." })),
263
267
  id: Type.Optional(Type.String({
@@ -309,12 +313,13 @@ const SubagentParamProperties = {
309
313
  description: "Agent config for create/update. Object or JSON string."
310
314
  })),
311
315
  workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript statement body. Normally async unless asyncByDefault:false; set async:true when async matters. Use async:false only when the parent must block until completion, never for reviews or gates. Use explicit return for output. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. Use await runs.run(key, {agent, task, worktree?, gate?}) or runs.run(key, {resume, task}), where resume is a retained run id or {workflowRunId,key,latest:true} from a durable async workflow receipt. Use runs.all([...]), await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}), runs.status(id), runs.ref(s), emit(value), console, and return. For ordinary parallel fanout, use await runs.all([{key, agent, task}, ...]); it resolves to an ordered array, not a key map, so use results[0], destructuring, or results.map(...), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout, and each must later be observed with direct await, Promise.race, or Promise.all. runs.steer targets a prior stable child key, never a raw run id, and must be awaited or returned. Mission workflows also have async state.get(key) and state.set(key, JSONValue). Compose sequential and parallel phases dynamically. Set worktree:true at workflow or child level for a separate managed worktree; child fields override workflow defaults. gate is one host-run command and cannot be combined with acceptance. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals." })),
316
+ workflowScriptPath: Type.Optional(Type.String({ minLength: 1, description: "Path to a trusted JavaScript workflow file. Mutually exclusive with workflowScript. Relative paths resolve against the request cwd. The host reads the file before the filesystem-free workflow sandbox starts." })),
312
317
  chatProgress: Type.Optional(Type.String({ enum: ["auto", "off", "live-card"], description: "WorkflowScript chat progress projection. auto shows a live in-chat card only for watched foreground workflows in the same Git repository; it is off otherwise. Explicit live-card requires same-repository async:false; async workflows should omit chatProgress or use auto/off." })),
313
318
  isolation: Type.Optional(Type.String({ enum: ["none", "worktree"], description: "Workflow child isolation. none runs in the shared cwd; worktree requires managed git worktree isolation." })),
314
319
  worktree: Type.Optional(Type.Boolean({ description: "Managed child isolation. true gives each workflow child a separate git worktree; an individual runs.run/runs.all item can override a workflow default with worktree:false." })),
315
320
  context: Type.Optional(Type.String({
316
321
  enum: ["fresh", "fork", "profile"],
317
- description: "'fresh' or 'fork' to branch from parent session, or 'profile' to require the selected agent's declared defaultContext. Explicit fresh/fork overrides every child; profile ignores config defaultSubagentContext and fails when an agent has no defaultContext. If omitted, config defaultSubagentContext wins over each agent defaultContext; implicit fork needs a persisted parent session and leaf, else fresh.",
322
+ description: "'fresh' or 'fork' to branch from parent session, or 'profile' to require the selected agent's declared defaultContext. Explicit fresh/fork overrides every child; profile ignores config defaultSubagentContext and fails when an agent has no defaultContext. If omitted, config defaultSubagentContext wins over each agent defaultContext; implicit fork needs a persisted parent session and leaf, else fresh. Config forkContext may prune resolved forks before spawn without adding another context value.",
318
323
  })),
319
324
  async: Type.Optional(Type.Boolean({ description: "Run in background unless asyncByDefault:false. Set false only when the parent must block until completion." })),
320
325
  timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Timeout. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline. Alias maxRuntimeMs." })),
@@ -343,6 +348,7 @@ const SubagentParamProperties = {
343
348
  outputMode: Type.Optional(OutputModeOverride),
344
349
  skill: Type.Optional(SkillOverride),
345
350
  model: Type.Optional(Type.String({ description: "Default child model override. Full provider/id values are accepted; bare ids resolve from the active registry." })),
351
+ 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
352
  outputSchema: Type.Optional(JsonSchemaObject),
347
353
  agentContract: Type.Optional(AgentContractOverride),
348
354
  acceptance: Type.Optional(AcceptanceOverride),