pi-subagents 0.56.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 (83) hide show
  1. package/CHANGELOG.md +53 -0
  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/observability.md +8 -7
  12. package/docs/tool-reference.md +11 -1
  13. package/docs/workflows.md +20 -0
  14. package/package.json +1 -1
  15. package/skills/pi-subagents/references/execution-controls.md +1 -1
  16. package/src/agents/agent-management.ts +13 -5
  17. package/src/agents/agent-refinements.ts +4 -4
  18. package/src/agents/agents.ts +219 -48
  19. package/src/agents/builtin-names.ts +6 -0
  20. package/src/agents/runtime-agent-registry.ts +8 -2
  21. package/src/api/preflight.ts +2 -2
  22. package/src/extension/config.ts +70 -0
  23. package/src/extension/doctor.ts +3 -3
  24. package/src/extension/index.ts +18 -4
  25. package/src/extension/public-execution.ts +29 -13
  26. package/src/extension/rpc.ts +20 -5
  27. package/src/extension/schemas.ts +4 -3
  28. package/src/extension/tool-description.ts +11 -9
  29. package/src/inspectors/herdr/actions.ts +2 -1
  30. package/src/inspectors/herdr/inspector-runner.ts +2 -10
  31. package/src/inspectors/herdr/session-roots-codec.ts +42 -0
  32. package/src/integrations/herdr-status.ts +46 -3
  33. package/src/runs/background/active-async-capacity.ts +77 -10
  34. package/src/runs/background/async-execution.ts +100 -9
  35. package/src/runs/background/async-job-tracker.ts +5 -0
  36. package/src/runs/background/async-retention.ts +20 -3
  37. package/src/runs/background/async-status.ts +5 -0
  38. package/src/runs/background/chain-root-attachment.ts +15 -1
  39. package/src/runs/background/fleet-view.ts +16 -10
  40. package/src/runs/background/inspect-rpc.ts +8 -8
  41. package/src/runs/background/result-files.ts +27 -14
  42. package/src/runs/background/result-watcher.ts +9 -5
  43. package/src/runs/background/run-status.ts +33 -6
  44. package/src/runs/background/scheduled-runs.ts +7 -1
  45. package/src/runs/background/subagent-runner.ts +177 -44
  46. package/src/runs/background/wait-completions.ts +4 -0
  47. package/src/runs/foreground/execution.ts +72 -10
  48. package/src/runs/foreground/foreground-control.ts +6 -0
  49. package/src/runs/foreground/foreground-history.ts +22 -1
  50. package/src/runs/foreground/subagent-executor.ts +270 -81
  51. package/src/runs/foreground/workflow-detach-reconcile.ts +16 -1
  52. package/src/runs/shared/child-protocol.ts +21 -7
  53. package/src/runs/shared/claude-code-adapter.ts +129 -0
  54. package/src/runs/shared/codex-exec-adapter.ts +129 -0
  55. package/src/runs/shared/cursor-agent-adapter.ts +114 -0
  56. package/src/runs/shared/external-cli-contract.ts +167 -0
  57. package/src/runs/shared/external-cli-preflight.ts +122 -0
  58. package/src/runs/shared/external-cli-runner.ts +348 -55
  59. package/src/runs/shared/model-exclusions.ts +57 -7
  60. package/src/runs/shared/model-fallback.ts +23 -1
  61. package/src/runs/shared/mutation-evidence.ts +7 -2
  62. package/src/runs/shared/nested-events.ts +3 -1
  63. package/src/runs/shared/nested-render.ts +2 -2
  64. package/src/runs/shared/process-signal.ts +13 -0
  65. package/src/runs/shared/run-history.ts +21 -1
  66. package/src/shared/fork-context.ts +21 -0
  67. package/src/shared/formatters.ts +7 -1
  68. package/src/shared/pruned-fork.ts +450 -0
  69. package/src/shared/session-file-trust.ts +19 -0
  70. package/src/shared/session-tokens.ts +14 -3
  71. package/src/shared/settings.ts +10 -2
  72. package/src/shared/shortcuts.ts +17 -0
  73. package/src/shared/types.ts +118 -9
  74. package/src/shared/workflow-child-permit.ts +116 -0
  75. package/src/slash/delegation-adapters.ts +0 -1
  76. package/src/slash/slash-commands.ts +8 -6
  77. package/src/tui/fleet-status.ts +27 -10
  78. package/src/tui/fleet-transcript.ts +11 -5
  79. package/src/tui/fleet.ts +28 -13
  80. package/src/tui/render.ts +47 -21
  81. package/src/workflows/scripted-workflow.ts +248 -27
  82. package/src/workflows/workflow-child-summary.ts +117 -0
  83. package/src/workflows/workflow-receipt.ts +119 -4
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
5
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
6
- import { findBlockingAgentDiagnostic, resolveAgentName, type AgentConfig, type AgentDiscoveryDiagnostic, type AgentScope } from "../../agents/agents.ts";
6
+ import { discoverAgents, findBlockingAgentDiagnostic, formatUnknownAgentError, resolveAgentName, unknownAgentDiagnosticContext, type AgentConfig, type AgentDiscoveryDiagnostic, type AgentScope, type UnknownAgentDiagnosticContext } from "../../agents/agents.ts";
7
7
  import { getArtifactsDir, getChainRunsDir, getProjectArtifactPackagingWarning, getProjectSubagentsDir } from "../../shared/artifacts.ts";
8
8
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
9
9
  import { createCapacityResilientJsonWriter } from "../../shared/capacity-resilient-json.ts";
@@ -56,12 +56,13 @@ import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skill
56
56
  import { buildAsyncRunnerSteps, DEFAULT_ASYNC_TIMEOUT_MS, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable, workflowAwaitedAsyncResultPath } from "../background/async-execution.ts";
57
57
  import { updateActiveRunIndex } from "../background/active-run-index.ts";
58
58
  import { steeringReceipt } from "../background/steering.ts";
59
- import { acquireActiveAsyncCapacity, ActiveAsyncCapacityError, getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession, transferActiveAsyncCapacity, type ActiveAsyncCapacityHandle } from "../background/active-async-capacity.ts";
59
+ import { acquireActiveAsyncCapacity, ActiveAsyncCapacityError, getActiveAsyncCapacitySnapshot, resolveAbandonedSlotReleaseAfterMs, resolveMaxActiveAsyncRunsPerSession, transferActiveAsyncCapacity, type ActiveAsyncCapacityHandle } from "../background/active-async-capacity.ts";
60
60
  import { isScheduledRunAction, type ScheduledRunAction } from "../background/scheduled-runs.ts";
61
61
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
62
62
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
63
63
  import { normalizeGateAcceptance, validateExecutionAcceptance } from "../shared/acceptance.ts";
64
64
  import { canPreferFork, createForkContextResolver, forkedChildRequiresThinkingOff, resolveSubagentLaunchContext } from "../../shared/fork-context.ts";
65
+ import { createPrunedForkSessionWriter } from "../../shared/pruned-fork.ts";
65
66
  import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
66
67
  import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
67
68
  import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
@@ -75,7 +76,7 @@ import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCe
75
76
  import { isAgentContractV1 } from "../shared/agent-contract.ts";
76
77
  import { normalizeExtensionBindings, type ExtensionBindings } from "../shared/extension-bindings.ts";
77
78
  import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
78
- import { cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-output.ts";
79
+ import { assertJsonSchemaObject, cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-output.ts";
79
80
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
80
81
  import { createTaskMutationArbiter } from "../shared/llm-intent-arbiter.ts";
81
82
  import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
@@ -113,6 +114,7 @@ import { isStoppableAsyncStatusStep, resolveAsyncStatusChild } from "../shared/c
113
114
  import { inspectSubagentStatus } from "../background/run-status.ts";
114
115
  import { getExternalJobProvider } from "../../api/external-job-provider.ts";
115
116
  import { externalJobFollowUpRequestDigest, externalJobFollowUpRequestId, externalJobFollowUpRunId, externalJobPromptDigest, externalJobStableJson } from "../shared/external-job-runner.ts";
117
+ import { externalCliReceiptMetadata, normalizeExternalCliRunnerStatus } from "../shared/external-cli-contract.ts";
116
118
  import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
117
119
  import { handleMissionAction, MISSION_ACTIONS } from "../../missions/actions.ts";
118
120
  import { attachMissionToLaunchResult, prepareMissionLaunch, writeMissionAsyncBinding, type MissionLaunchBinding } from "../../missions/lifecycle.ts";
@@ -122,9 +124,11 @@ import { createMissionWorkflowState } from "../../missions/workflow-state.ts";
122
124
  import { resolveAuthorityDecision } from "../../policy/authority.ts";
123
125
  import { handleHerdrInspectorAction, HERDR_INSPECTOR_ACTIONS } from "../../inspectors/herdr/actions.ts";
124
126
  import { handleHerdrProjectPaneAction, HERDR_PROJECT_PANE_ACTIONS } from "../../inspectors/herdr/project-panes.ts";
125
- import { previewSimpleWorkflowRun, runWorkflowScript, WorkflowScriptError, type WorkflowReceiptResumeReference, type WorkflowScriptChildResult, type WorkflowSteerOptions, type WorkflowSteerResult } from "../../workflows/scripted-workflow.ts";
127
+ import { previewSimpleWorkflowRun, runWorkflowScript, validateWorkflowScript, WorkflowScriptError, type WorkflowReceiptResumeReference, type WorkflowScriptChildResult, type WorkflowSteerOptions, type WorkflowSteerResult } from "../../workflows/scripted-workflow.ts";
126
128
  import { buildWorkflowReceipt, resolveWorkflowReceiptResumeEntry, writeWorkflowReceipt, type WorkflowReceipt, type WorkflowReceiptState } from "../../workflows/workflow-receipt.ts";
129
+ import { workflowChildSummary } from "../../workflows/workflow-child-summary.ts";
127
130
  import { resolveWorkflowChatProgress, type WorkflowChatProgressProjection } from "../../workflows/chat-progress.ts";
131
+ import { claimWorkflowChildPermit, validateWorkflowChildPermitRoot, type WorkflowChildPermit, type WorkflowChildPermitContext } from "../../shared/workflow-child-permit.ts";
128
132
  import {
129
133
  cleanupWorktrees,
130
134
  createWorktrees,
@@ -146,6 +150,7 @@ import {
146
150
  type ControlEvent,
147
151
  type Details,
148
152
  type ExtensionConfig,
153
+ type ForegroundResumeChild,
149
154
  type ForegroundRunControl,
150
155
  type IntercomBridgeConfig,
151
156
  type IntercomEventBus,
@@ -153,6 +158,7 @@ import {
153
158
  type MaxOutputConfig,
154
159
  type NestedRouteInfo,
155
160
  type NestedRunSummary,
161
+ type OutputMode,
156
162
  type ResolvedControlConfig,
157
163
  type ResolvedTurnBudget,
158
164
  type ResolvedToolBudget,
@@ -298,6 +304,7 @@ export interface SubagentParamsLike {
298
304
  steeringRecovery?: boolean;
299
305
  mode?: SteerDeliveryMode;
300
306
  workflowScript?: string;
307
+ workflowScriptPath?: string;
301
308
  chatProgress?: "auto" | "off" | "live-card";
302
309
  isolation?: "none" | "worktree";
303
310
  step?: ChainStep;
@@ -397,7 +404,7 @@ interface ExecutorDeps {
397
404
  tempArtifactsDir: string;
398
405
  getSubagentSessionRoot: (parentSessionFile: string | null) => string;
399
406
  expandTilde: (p: string) => string;
400
- discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[]; agentDiagnostics?: AgentDiscoveryDiagnostic[]; modelScope?: ModelScopeConfig; maxThinking?: AgentConfig["maxThinking"] };
407
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[]; agentDiagnostics?: AgentDiscoveryDiagnostic[]; modelScope?: ModelScopeConfig; maxThinking?: AgentConfig["maxThinking"]; cwd?: string; scope?: AgentScope; directories?: UnknownAgentDiagnosticContext["directories"] };
401
408
  allowMutatingManagementActions?: boolean;
402
409
  activateSupervisorTransport?: () => void;
403
410
  refreshResultDelivery?: () => void;
@@ -405,6 +412,7 @@ interface ExecutorDeps {
405
412
  }
406
413
 
407
414
  type ForkSessionFileForTask = (agentName: string, idx?: number, modelOverride?: string, modelOverrideFromParent?: boolean) => string | undefined;
415
+ type PrepareForkSessionForTask = (agentName: string, idx?: number, modelOverride?: string, modelOverrideFromParent?: boolean) => Promise<void>;
408
416
  type ForkThinkingOverrideForTask = (agentName: string, idx?: number, modelOverride?: string, modelOverrideFromParent?: boolean) => AgentConfig["thinking"] | undefined;
409
417
 
410
418
  interface ExecutionContextData {
@@ -414,6 +422,8 @@ interface ExecutionContextData {
414
422
  signal: AbortSignal;
415
423
  onUpdate?: (r: AgentToolResult<Details>) => void;
416
424
  agents: AgentConfig[];
425
+ /** Exact filesystem discovery provenance retained through defensive execution backstops. */
426
+ unknownAgentDiagnosticContext: UnknownAgentDiagnosticContext;
417
427
  /** Discovered agent definitions before per-run bridge injection. */
418
428
  recoveryAgents: AgentConfig[];
419
429
  runId: string;
@@ -449,12 +459,27 @@ interface ExecutionContextData {
449
459
  runFanoutBudget: RunFanoutBudgetDescriptor;
450
460
  topLevelAsyncCapacityEligible: boolean;
451
461
  activeAsyncCapacity?: ActiveAsyncCapacityHandle;
462
+ workflowChildPermitLaunch?: WorkflowChildPermitContext;
452
463
  }
453
464
 
454
465
  function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
455
466
  return requestedCwd ? path.resolve(runtimeCwd, requestedCwd) : runtimeCwd;
456
467
  }
457
468
 
469
+ function loadWorkflowScriptPath(params: SubagentParamsLike, runtimeCwd: string): { params?: SubagentParamsLike; error?: string } {
470
+ if (params.workflowScriptPath === undefined) return { params };
471
+ const scriptPath = path.resolve(resolveRequestedCwd(runtimeCwd, params.cwd), params.workflowScriptPath);
472
+ let workflowScript: string;
473
+ try {
474
+ workflowScript = fs.readFileSync(scriptPath, "utf8");
475
+ } catch (error) {
476
+ return { error: `Failed to read workflowScriptPath '${scriptPath}': ${error instanceof Error ? error.message : String(error)}` };
477
+ }
478
+ if (!workflowScript.trim()) return { error: `workflowScriptPath file '${scriptPath}' is empty.` };
479
+ const { workflowScriptPath: _workflowScriptPath, ...rest } = params;
480
+ return { params: { ...rest, workflowScript } };
481
+ }
482
+
458
483
  function removeForegroundControlIfIdle(state: SubagentState, runId: string): boolean {
459
484
  const control = state.foregroundControls.get(runId);
460
485
  if (control && (!foregroundSchedulingSettled(control) || (control.activeChildren?.size ?? 0) > 0)) return false;
@@ -543,6 +568,7 @@ function withSpawnBudgetStatus(
543
568
  const activeAsyncCapacity = sessionId
544
569
  ? getActiveAsyncCapacitySnapshot(sessionId, resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession), {
545
570
  liveWorkflowRunIds: new Set(state.workflowControllers?.keys() ?? []),
571
+ abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(config.capacity?.abandonedSlotReleaseAfterMs),
546
572
  })
547
573
  : { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 };
548
574
  state.activeAsyncCapacity = activeAsyncCapacity;
@@ -645,11 +671,13 @@ function foregroundChildActivityFromProgress(progress: SingleResult["progress"]
645
671
  ...(progress?.currentPath ? { currentPath: progress.currentPath } : {}),
646
672
  ...(progress?.turnCount !== undefined ? { turnCount: progress.turnCount } : {}),
647
673
  ...(progress?.tokens !== undefined ? { tokens: progress.tokens } : {}),
674
+ ...(progress?.window !== undefined ? { window: progress.window } : {}),
675
+ ...(progress?.windowPeak !== undefined ? { windowPeak: progress.windowPeak } : {}),
648
676
  ...(progress?.toolCount !== undefined ? { toolCount: progress.toolCount } : {}),
649
677
  };
650
678
  }
651
679
 
652
- function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[]; extensionBindings?: ExtensionBindings }): void {
680
+ function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[]; params: SubagentParamsLike; effectiveOutput?: string | boolean; effectiveOutputMode: OutputMode; extensionBindings?: ExtensionBindings }): void {
653
681
  state.foregroundRuns ??= new Map();
654
682
  const previous = state.foregroundRuns.get(input.runId);
655
683
  const updatedAt = Date.now();
@@ -660,6 +688,13 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
660
688
  ...(input.sessionId ? { sessionId: input.sessionId } : {}),
661
689
  updatedAt,
662
690
  children: input.results.map((result, index) => {
691
+ const resumeContract = omitUndefinedProperties({
692
+ outputSchema: input.params.outputSchema,
693
+ agentContract: input.params.agentContract,
694
+ acceptance: input.params.acceptance,
695
+ output: input.effectiveOutput,
696
+ outputMode: input.effectiveOutputMode,
697
+ });
663
698
  const child = {
664
699
  agent: result.agent,
665
700
  index,
@@ -690,6 +725,7 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
690
725
  ...(result.transcriptError ? { transcriptError: result.transcriptError } : {}),
691
726
  ...(result.detachedReason ? { detachedReason: result.detachedReason } : {}),
692
727
  ...(result.acceptance ? { acceptance: result.acceptance } : {}),
728
+ ...(Object.keys(resumeContract).length ? { resumeContract } : {}),
693
729
  ...(result.launchContractDigest ? { launchContractDigest: result.launchContractDigest } : {}),
694
730
  ...(input.extensionBindings ? { extensionBindings: input.extensionBindings } : {}),
695
731
  ...(result.launchResolvedExtensions ? { launchResolvedExtensions: result.launchResolvedExtensions } : {}),
@@ -810,7 +846,7 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
810
846
  });
811
847
  }
812
848
 
813
- function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState, options: { exactOnly?: boolean } = {}): { runId: string; mode: SubagentRunMode; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string; model?: string; thinking?: string; launchContractDigest?: string; extensionBindings?: ExtensionBindings; capabilityCeiling?: ResolvedSubagentCapabilityCeiling } | undefined {
849
+ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState, options: { exactOnly?: boolean } = {}): { runId: string; mode: SubagentRunMode; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string; model?: string; thinking?: string; launchContractDigest?: string; resumeContract?: ForegroundResumeChild["resumeContract"]; extensionBindings?: ExtensionBindings; capabilityCeiling?: ResolvedSubagentCapabilityCeiling } | undefined {
814
850
  const requested = (params.id ?? params.runId)?.trim();
815
851
  if (!requested || !state.foregroundRuns?.size || !state.currentSessionId) return undefined;
816
852
  const direct = state.foregroundRuns.get(requested);
@@ -841,6 +877,7 @@ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: Subage
841
877
  ...(child.model ? { model: child.model } : {}),
842
878
  ...(child.thinking ? { thinking: child.thinking } : {}),
843
879
  ...(child.launchContractDigest ? { launchContractDigest: child.launchContractDigest } : {}),
880
+ ...(child.resumeContract ? { resumeContract: child.resumeContract } : {}),
844
881
  ...(child.extensionBindings ? { extensionBindings: normalizeExtensionBindings(child.extensionBindings)!.value } : {}),
845
882
  ...(child.capabilityCeiling ? { capabilityCeiling: child.capabilityCeiling } : {}),
846
883
  };
@@ -1239,6 +1276,7 @@ function appendStepToAsyncChain(input: {
1239
1276
  agents,
1240
1277
  ctx: asyncCtx,
1241
1278
  availableModels: input.ctx.modelRegistry.getAvailable().map(toModelInfo),
1279
+ unknownAgentDiagnosticContext: diagnosticContextFromDiscovery(discoveredForAppend, input.requestCwd, scope),
1242
1280
  cwd: status.cwd ?? input.requestCwd,
1243
1281
  chainSkills,
1244
1282
  dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
@@ -1514,9 +1552,14 @@ function externalJobFollowUpStarted(input: { sourceRunId: string; runId: string;
1514
1552
  function externalRunnerControlError(asyncDir: string, action: "steer" | "resume"): AgentToolResult<Details> | undefined {
1515
1553
  const status = readStatus(asyncDir);
1516
1554
  if (!status?.steps?.length || !status.steps.every((step) => step.runner?.type === "external-cli" || step.runner?.type === "external-job")) return undefined;
1517
- const message = action === "steer"
1518
- ? "External runners do not accept live steer messages."
1519
- : "External runners do not persist Pi sessions and cannot be resumed.";
1555
+ const externalCli = normalizeExternalCliRunnerStatus(status.steps.find((step) => step.runner?.type === "external-cli")?.runner);
1556
+ const message = externalCli
1557
+ ? action === "steer"
1558
+ ? `External adapter '${externalCli.adapter.id}' does not support runs.steer: ${externalCli.unsupportedReasons.steer}`
1559
+ : `External adapter '${externalCli.adapter.id}' cannot resume: ${externalCli.nonResumableReason}`
1560
+ : action === "steer"
1561
+ ? "External runners do not accept live steer messages."
1562
+ : "External runners do not persist Pi sessions and cannot be resumed.";
1520
1563
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
1521
1564
  }
1522
1565
 
@@ -1572,7 +1615,7 @@ async function resumeExternalJobFollowUp(input: {
1572
1615
  runId,
1573
1616
  kind: "runner",
1574
1617
  asyncDir,
1575
- }, { liveWorkflowRunIds: new Set(input.deps.state.workflowControllers?.keys() ?? []) }) : undefined;
1618
+ }, { liveWorkflowRunIds: new Set(input.deps.state.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(input.deps.config.capacity?.abandonedSlotReleaseAfterMs) }) : undefined;
1576
1619
  } catch (error) {
1577
1620
  if (error instanceof ActiveAsyncCapacityError) return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: "single", results: [], activeAsyncCapacity: error.snapshot } };
1578
1621
  return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "single", results: [] } };
@@ -1729,6 +1772,7 @@ async function resumeAsyncRun(input: {
1729
1772
  const scope: AgentScope = resolveExecutionAgentScope(input.params.agentScope);
1730
1773
  const discovered = input.deps.discoverAgents(effectiveCwd, scope);
1731
1774
  const discoveredAgents = discovered.agents;
1775
+ const unknownAgentDiagnosticContext = diagnosticContextFromDiscovery(discovered, effectiveCwd, scope);
1732
1776
  const modelScope = discovered.modelScope;
1733
1777
  const sessionName = resolveIntercomSessionTarget(input.deps.pi.getSessionName(), input.ctx.sessionManager.getSessionId());
1734
1778
  const recoveryDescriptor = "recoveryDescriptor" in target ? target.recoveryDescriptor : undefined;
@@ -1756,13 +1800,17 @@ async function resumeAsyncRun(input: {
1756
1800
  } : undefined);
1757
1801
  if (!baseAgentConfig) {
1758
1802
  return {
1759
- content: [{ type: "text", text: `Unknown agent for resume: ${target.agent}` }],
1803
+ content: [{ type: "text", text: formatUnknownAgentError(target.agent, unknownAgentDiagnosticContext, "Unknown agent for resume") }],
1760
1804
  isError: true,
1761
1805
  details: { mode: "management", results: [] },
1762
1806
  };
1763
1807
  }
1764
1808
  if (target.source === "async" && target.runner?.type === "external-cli") {
1765
- return { content: [{ type: "text", text: "External runners do not persist Pi sessions and cannot be resumed." }], isError: true, details: { mode: "management", results: [] } };
1809
+ const runner = normalizeExternalCliRunnerStatus(target.runner);
1810
+ const message = runner
1811
+ ? `External adapter '${runner.adapter.id}' cannot resume: ${runner.nonResumableReason}`
1812
+ : "External runners do not persist Pi sessions and cannot be resumed.";
1813
+ return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
1766
1814
  }
1767
1815
  if (target.source === "async" && target.runner?.type === "external-job") {
1768
1816
  if (attachChain) return { content: [{ type: "text", text: "External-job follow-up does not support chain attachment. Use action='resume' with message instead." }], isError: true, details: { mode: "management", results: [] } };
@@ -1807,7 +1855,7 @@ async function resumeAsyncRun(input: {
1807
1855
  runId,
1808
1856
  kind: "runner",
1809
1857
  asyncDir: path.join(DIRS.async, runId),
1810
- }, { liveWorkflowRunIds: new Set(input.deps.state.workflowControllers?.keys() ?? []) }) : undefined;
1858
+ }, { liveWorkflowRunIds: new Set(input.deps.state.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(input.deps.config.capacity?.abandonedSlotReleaseAfterMs) }) : undefined;
1811
1859
  } catch (error) {
1812
1860
  if (error instanceof ActiveAsyncCapacityError) return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: "chain", results: [], activeAsyncCapacity: error.snapshot } };
1813
1861
  throw error;
@@ -1833,6 +1881,7 @@ async function resumeAsyncRun(input: {
1833
1881
  label: `Attached ${target.runId}`,
1834
1882
  },
1835
1883
  agents,
1884
+ unknownAgentDiagnosticContext,
1836
1885
  ctx: compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
1837
1886
  pi: input.deps.pi,
1838
1887
  cwd: input.requestCwd,
@@ -1905,20 +1954,23 @@ async function resumeAsyncRun(input: {
1905
1954
  sourceRunId: target.runId,
1906
1955
  runId,
1907
1956
  asyncDir: path.join(DIRS.async, runId),
1908
- })
1957
+ }, { abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(input.deps.config.capacity?.abandonedSlotReleaseAfterMs) })
1909
1958
  : acquireActiveAsyncCapacity({
1910
1959
  sessionId: input.deps.state.currentSessionId!,
1911
1960
  limit: resolveMaxActiveAsyncRunsPerSession(input.deps.config.maxActiveAsyncRunsPerSession),
1912
1961
  runId,
1913
1962
  kind: "runner",
1914
1963
  asyncDir: path.join(DIRS.async, runId),
1915
- });
1964
+ }, { abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(input.deps.config.capacity?.abandonedSlotReleaseAfterMs) });
1916
1965
  } catch (error) {
1917
1966
  if (error instanceof ActiveAsyncCapacityError) return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: "single", results: [], activeAsyncCapacity: error.snapshot } };
1918
1967
  return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "single", results: [] } };
1919
1968
  }
1920
1969
  const recoveryAgentConfig = recoveryDescriptor ? applySteeringRecoveryAgentConfig(baseAgentConfig, recoveryDescriptor) : baseAgentConfig;
1921
1970
  const agentConfig = intercomBridge.active ? applyIntercomBridgeToAgent(recoveryAgentConfig, intercomBridge) : recoveryAgentConfig;
1971
+ const foregroundContract = target.source === "foreground" ? target.resumeContract : undefined;
1972
+ const outputSchema = input.params.outputSchema ?? foregroundContract?.outputSchema ?? recoveryDescriptor?.structuredOutputSchema;
1973
+ const agentContract = input.params.agentContract ?? foregroundContract?.agentContract ?? recoveryDescriptor?.agentContract;
1922
1974
  const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? omitUndefinedProperties({ ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir });
1923
1975
  const artifactsDir = recoveryDescriptor?.artifactsDir ?? getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
1924
1976
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
@@ -1974,12 +2026,12 @@ async function resumeAsyncRun(input: {
1974
2026
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
1975
2027
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1976
2028
  availableModels,
1977
- output: typeof input.params.output === "string" ? input.params.output : recoveryDescriptor?.outputPath,
1978
- outputMode: input.params.outputMode ?? recoveryDescriptor?.outputMode,
1979
- ...(recoveryDescriptor?.agentContract ? { agentContract: recoveryDescriptor.agentContract } : {}),
1980
- ...(recoveryDescriptor?.structuredOutputSchema ? { structuredOutputSchema: recoveryDescriptor.structuredOutputSchema } : {}),
2029
+ output: input.params.output !== undefined ? input.params.output : foregroundContract?.output ?? recoveryDescriptor?.outputPath,
2030
+ outputMode: input.params.outputMode ?? foregroundContract?.outputMode ?? recoveryDescriptor?.outputMode,
2031
+ ...(agentContract ? { agentContract } : {}),
2032
+ ...(outputSchema ? { structuredOutputSchema: outputSchema } : {}),
1981
2033
  ...(recoveryDescriptor?.skills ? { skills: [...recoveryDescriptor.skills] } : {}),
1982
- ...(recoveryDescriptor?.acceptance !== undefined && input.params.acceptance === undefined ? { acceptance: recoveryDescriptor.acceptance } : {}),
2034
+ ...(input.params.acceptance !== undefined ? { acceptance: input.params.acceptance } : foregroundContract?.acceptance !== undefined ? { acceptance: foregroundContract.acceptance } : recoveryDescriptor?.acceptance !== undefined ? { acceptance: recoveryDescriptor.acceptance } : {}),
1983
2035
  ...(input.params.timeoutMs !== undefined ? { timeoutMs: input.params.timeoutMs } : {}),
1984
2036
  ...(input.absoluteDeadlineAt !== undefined ? { absoluteDeadlineAt: input.absoluteDeadlineAt } : {}),
1985
2037
  ...(input.params.turnBudget !== undefined ? { turnBudget: input.params.turnBudget } : {}),
@@ -2058,9 +2110,13 @@ async function resumeAsyncRun(input: {
2058
2110
  ...(completed.structuredOutputPath ? { structuredOutputPath: completed.structuredOutputPath } : {}),
2059
2111
  ...(completed.structuredOutputSchemaPath ? { structuredOutputSchemaPath: completed.structuredOutputSchemaPath } : {}),
2060
2112
  ...(completed.acceptance ? { acceptance: completed.acceptance } : {}),
2113
+ ...(completed.artifactPaths ? { artifactPaths: completed.artifactPaths } : {}),
2114
+ ...(completed.outputSaveError ? { outputSaveError: completed.outputSaveError } : {}),
2115
+ ...(completed.transcriptPath ? { transcriptPath: completed.transcriptPath } : {}),
2116
+ ...(completed.transcriptError ? { transcriptError: completed.transcriptError } : {}),
2061
2117
  };
2062
2118
  return {
2063
- content: [{ type: "text", text: completed.output || completed.error || `Revived ${target.source} subagent ${revivedId} completed without output.` }],
2119
+ content: [{ type: "text", text: completed.success ? completed.output || completed.error || `Revived ${target.source} subagent ${revivedId} completed without output.` : completed.error || completed.output || `Revived ${target.source} subagent ${revivedId} completed without output.` }],
2064
2120
  ...(completed.success ? {} : { isError: true }),
2065
2121
  details: {
2066
2122
  ...result.details,
@@ -2225,19 +2281,28 @@ async function maybeBuildForegroundIntercomReceipt(input: {
2225
2281
  };
2226
2282
  }
2227
2283
 
2228
- function canonicalizeAgentName(name: string, agents: AgentConfig[], diagnostics?: AgentDiscoveryDiagnostic[]): { name?: string; error?: string } {
2284
+ function diagnosticContextFromDiscovery(
2285
+ discovered: { agents: AgentConfig[]; cwd?: string; scope?: AgentScope; directories?: UnknownAgentDiagnosticContext["directories"] },
2286
+ cwd: string,
2287
+ scope: AgentScope,
2288
+ ): UnknownAgentDiagnosticContext {
2289
+ if (discovered.cwd && discovered.scope && discovered.directories) return unknownAgentDiagnosticContext({ ...discovered, cwd: discovered.cwd, scope: discovered.scope, directories: discovered.directories });
2290
+ return unknownAgentDiagnosticContext(discoverAgents(path.resolve(cwd), scope));
2291
+ }
2292
+
2293
+ function canonicalizeAgentName(name: string, agents: AgentConfig[], diagnostics: AgentDiscoveryDiagnostic[] | undefined, context: UnknownAgentDiagnosticContext): { name?: string; error?: string } {
2229
2294
  const resolved = resolveAgentName(name, agents);
2230
2295
  const candidates = resolved.error ? agents.filter((agent) => resolveAgentName(name, [agent]).agent) : resolved.agent;
2231
2296
  const diagnostic = findBlockingAgentDiagnostic(name, candidates, diagnostics);
2232
2297
  if (diagnostic) return { error: `Agent '${name}' has invalid configuration: ${diagnostic.error}` };
2233
2298
  if (resolved.error) return { error: resolved.error };
2234
- if (!resolved.agent) return { error: `Unknown agent: ${name}` };
2299
+ if (!resolved.agent) return { error: formatUnknownAgentError(name, context) };
2235
2300
  return { name: resolved.agent.name };
2236
2301
  }
2237
2302
 
2238
- function canonicalizeExecutionParams(params: SubagentParamsLike, agents: AgentConfig[], diagnostics?: AgentDiscoveryDiagnostic[]): { params?: SubagentParamsLike; error?: string } {
2303
+ function canonicalizeExecutionParams(params: SubagentParamsLike, agents: AgentConfig[], diagnostics: AgentDiscoveryDiagnostic[] | undefined, context: UnknownAgentDiagnosticContext): { params?: SubagentParamsLike; error?: string } {
2239
2304
  const resolve = (name: string, location?: string): { name?: string; error?: string } => {
2240
- const result = canonicalizeAgentName(name, agents, diagnostics);
2305
+ const result = canonicalizeAgentName(name, agents, diagnostics, context);
2241
2306
  return result.error && location ? { error: `${result.error} (${location})` } : result;
2242
2307
  };
2243
2308
  if (params.agent) {
@@ -2305,6 +2370,7 @@ function validateExecutionInput(
2305
2370
  hasTasks: boolean,
2306
2371
  hasSingle: boolean,
2307
2372
  allowClarifyTaskPrompt: boolean,
2373
+ context: UnknownAgentDiagnosticContext,
2308
2374
  ): AgentToolResult<Details> | null {
2309
2375
  if (Number(hasChain) + Number(hasTasks) + Number(hasSingle) !== 1) {
2310
2376
  return {
@@ -2330,7 +2396,7 @@ function validateExecutionInput(
2330
2396
 
2331
2397
  if (hasSingle && params.agent && !agents.find((agent) => agent.name === params.agent)) {
2332
2398
  return {
2333
- content: [{ type: "text", text: `Unknown agent: ${params.agent}` }],
2399
+ content: [{ type: "text", text: formatUnknownAgentError(params.agent, context) }],
2334
2400
  isError: true,
2335
2401
  details: { mode: "single" as const, results: [] },
2336
2402
  };
@@ -2341,7 +2407,7 @@ function validateExecutionInput(
2341
2407
  const task = params.tasks[i]!;
2342
2408
  if (!agents.find((agent) => agent.name === task.agent)) {
2343
2409
  return {
2344
- content: [{ type: "text", text: `Unknown agent: ${task.agent} (task ${i + 1})` }],
2410
+ content: [{ type: "text", text: `${formatUnknownAgentError(task.agent, context)} (task ${i + 1})` }],
2345
2411
  isError: true,
2346
2412
  details: { mode: "parallel" as const, results: [] },
2347
2413
  };
@@ -2386,7 +2452,7 @@ function validateExecutionInput(
2386
2452
  for (const agentName of stepAgents) {
2387
2453
  if (!agents.find((a) => a.name === agentName)) {
2388
2454
  return {
2389
- content: [{ type: "text", text: `Unknown agent: ${agentName} (step ${i + 1})` }],
2455
+ content: [{ type: "text", text: `${formatUnknownAgentError(agentName, context)} (step ${i + 1})` }],
2390
2456
  isError: true,
2391
2457
  details: { mode: "chain" as const, results: [] },
2392
2458
  };
@@ -2977,21 +3043,21 @@ function wrapChainTasksForFork(chain: ChainStep[], contextPolicy: AgentDefaultCo
2977
3043
  });
2978
3044
  }
2979
3045
 
2980
- function preflightForkSessionsForStaticTasks(
3046
+ async function preflightForkSessionsForStaticTasks(
2981
3047
  params: SubagentParamsLike,
2982
3048
  contextPolicy: AgentDefaultContextPolicy,
2983
- sessionFileForTask: ForkSessionFileForTask,
3049
+ prepareSessionForTask: PrepareForkSessionForTask,
2984
3050
  dynamicFanoutMaxItems?: number,
2985
- ): void {
3051
+ ): Promise<void> {
2986
3052
  if (!contextPolicy.usesFork) return;
2987
3053
  if (params.agent) {
2988
- if (shouldForkAgent(contextPolicy, params.agent)) sessionFileForTask(params.agent, 0, params.model);
3054
+ if (shouldForkAgent(contextPolicy, params.agent)) await prepareSessionForTask(params.agent, 0, params.model);
2989
3055
  return;
2990
3056
  }
2991
3057
  if (params.tasks) {
2992
- params.tasks.forEach((task, index) => {
2993
- if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, index, task.model);
2994
- });
3058
+ for (const [index, task] of params.tasks.entries()) {
3059
+ if (shouldForkAgent(contextPolicy, task.agent)) await prepareSessionForTask(task.agent, index, task.model);
3060
+ }
2995
3061
  return;
2996
3062
  }
2997
3063
  if (!params.chain?.length) return;
@@ -2999,7 +3065,7 @@ function preflightForkSessionsForStaticTasks(
2999
3065
  for (const step of params.chain) {
3000
3066
  if (isParallelStep(step)) {
3001
3067
  for (const task of step.parallel) {
3002
- if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, flatIndex, task.model);
3068
+ if (shouldForkAgent(contextPolicy, task.agent)) await prepareSessionForTask(task.agent, flatIndex, task.model);
3003
3069
  flatIndex++;
3004
3070
  }
3005
3071
  continue;
@@ -3007,13 +3073,13 @@ function preflightForkSessionsForStaticTasks(
3007
3073
  if (isDynamicParallelStep(step)) {
3008
3074
  const maxItems = step.expand.maxItems ?? dynamicFanoutMaxItems ?? 0;
3009
3075
  if (shouldForkAgent(contextPolicy, step.parallel.agent)) {
3010
- for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) sessionFileForTask(step.parallel.agent, flatIndex + itemIndex, step.parallel.model);
3076
+ for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) await prepareSessionForTask(step.parallel.agent, flatIndex + itemIndex, step.parallel.model);
3011
3077
  }
3012
3078
  flatIndex += maxItems;
3013
3079
  continue;
3014
3080
  }
3015
3081
  const sequential = step as SequentialStep;
3016
- if (shouldForkAgent(contextPolicy, sequential.agent)) sessionFileForTask(sequential.agent, flatIndex, sequential.model);
3082
+ if (shouldForkAgent(contextPolicy, sequential.agent)) await prepareSessionForTask(sequential.agent, flatIndex, sequential.model);
3017
3083
  flatIndex++;
3018
3084
  }
3019
3085
  }
@@ -3067,9 +3133,13 @@ async function waitForWorkflowAsyncSingleResult(
3067
3133
  ...(completed.structuredOutputPath ? { structuredOutputPath: completed.structuredOutputPath } : {}),
3068
3134
  ...(completed.structuredOutputSchemaPath ? { structuredOutputSchemaPath: completed.structuredOutputSchemaPath } : {}),
3069
3135
  ...(completed.acceptance ? { acceptance: completed.acceptance } : {}),
3136
+ ...(completed.artifactPaths ? { artifactPaths: completed.artifactPaths } : {}),
3137
+ ...(completed.outputSaveError ? { outputSaveError: completed.outputSaveError } : {}),
3138
+ ...(completed.transcriptPath ? { transcriptPath: completed.transcriptPath } : {}),
3139
+ ...(completed.transcriptError ? { transcriptError: completed.transcriptError } : {}),
3070
3140
  });
3071
3141
  return {
3072
- content: [{ type: "text", text: completed.output || completed.error || `Async workflow child ${options.runId} completed without output.` }],
3142
+ content: [{ type: "text", text: completed.success ? completed.output || completed.error || `Async workflow child ${options.runId} completed without output.` : completed.error || completed.output || `Async workflow child ${options.runId} completed without output.` }],
3073
3143
  ...(completed.success ? {} : { isError: true }),
3074
3144
  details: {
3075
3145
  ...launchResult.details,
@@ -3097,6 +3167,7 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
3097
3167
  intercomBridge,
3098
3168
  nestedRoute,
3099
3169
  contextPolicy,
3170
+ unknownAgentDiagnosticContext,
3100
3171
  } = data;
3101
3172
  const hasChain = (params.chain?.length ?? 0) > 0;
3102
3173
  const hasTasks = (params.tasks?.length ?? 0) > 0;
@@ -3135,7 +3206,7 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
3135
3206
  const a = agents.find((x) => x.name === params.agent);
3136
3207
  if (!a) {
3137
3208
  return {
3138
- content: [{ type: "text", text: `Unknown agent: ${params.agent}` }],
3209
+ content: [{ type: "text", text: formatUnknownAgentError(params.agent!, unknownAgentDiagnosticContext) }],
3139
3210
  isError: true,
3140
3211
  details: { mode: "single" as const, results: [] },
3141
3212
  };
@@ -3501,7 +3572,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3501
3572
  const agentConfig = agents.find((a) => a.name === params.agent);
3502
3573
  if (!agentConfig) {
3503
3574
  return {
3504
- content: [{ type: "text", text: `Unknown agent: ${params.agent}` }],
3575
+ content: [{ type: "text", text: formatUnknownAgentError(params.agent!, data.unknownAgentDiagnosticContext) }],
3505
3576
  isError: true,
3506
3577
  details: { mode: "single", results: [] },
3507
3578
  };
@@ -3634,6 +3705,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3634
3705
  llmIntentArbiter: createTaskMutationArbiter(ctx),
3635
3706
  ...workflowForegroundSteeringLaunchOptions(foregroundControl, 0),
3636
3707
  context: data.contextPolicy.contextForAgent(params.agent!),
3708
+ unknownAgentDiagnosticContext: data.unknownAgentDiagnosticContext,
3637
3709
  runFanoutBudget: params.runFanoutAdmitted ? data.runFanoutBudget : { ...data.runFanoutBudget, parentPath: `${data.runFanoutBudget.parentPath ? `${data.runFanoutBudget.parentPath}/` : ""}single` },
3638
3710
  cwd: singleCwd,
3639
3711
  signal,
@@ -3672,6 +3744,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3672
3744
  agentContract: params.agentContract,
3673
3745
  acceptance: params.acceptance,
3674
3746
  acceptanceContext: { mode: "single" },
3747
+ workflowChildPermitLaunch: data.workflowChildPermitLaunch,
3675
3748
  onEffectivePrompt: foregroundControl ? (prompt) => updateLiveEffectivePrompt(foregroundControl, 0, prompt) : undefined,
3676
3749
  onDetachReady: (detach) => {
3677
3750
  detachForeground = detach;
@@ -3705,7 +3778,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3705
3778
  }
3706
3779
  }
3707
3780
  }
3708
- recordRun(params.agent!, cleanTask, result.exitCode, result.progressSummary?.durationMs ?? 0);
3781
+ recordRun(params.agent!, cleanTask, result.exitCode, result.progressSummary?.durationMs ?? 0, result);
3709
3782
  },
3710
3783
  timeoutMs: data.timeoutMs,
3711
3784
  deadlineAt,
@@ -3730,7 +3803,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3730
3803
  }
3731
3804
  }
3732
3805
  if (!r.detached) {
3733
- recordRun(params.agent!, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
3806
+ recordRun(params.agent!, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0, r);
3734
3807
  }
3735
3808
 
3736
3809
  let worktreeHandoff: ReturnType<typeof finalizeSingleWorktreeHandoff> | undefined;
@@ -3776,7 +3849,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3776
3849
  usageBudget: usageBudgetState(data.usageBudget, totalCost),
3777
3850
  ...(worktreeHandoff?.reference ? { parallelHandoff: worktreeHandoff.reference } : {}),
3778
3851
  }));
3779
- rememberForegroundRun(deps.state, { runId, mode: "single", cwd: singleCwd, sessionId: data.parentSessionId, results: details.results, extensionBindings: params.extensionBindings });
3852
+ rememberForegroundRun(deps.state, { runId, mode: "single", cwd: singleCwd, sessionId: data.parentSessionId, results: details.results, params, effectiveOutput, effectiveOutputMode, extensionBindings: params.extensionBindings });
3780
3853
 
3781
3854
  const suppressRoutineResultIntercom = shouldSuppressRoutineResultIntercom({ suppressRoutineResultIntercom: params.suppressRoutineResultIntercom, results: [r] });
3782
3855
  if (!r.detached && !r.interrupted && !suppressRoutineResultIntercom) {
@@ -3930,6 +4003,10 @@ function workflowChildResult(
3930
4003
  const output = result.details.results.length === 1 && result.details.results[0]?.finalOutput !== undefined
3931
4004
  ? result.details.results[0].finalOutput
3932
4005
  : receiptOutput;
4006
+ const childError = result.details.results.map((child) => child.error).find((error): error is string => Boolean(error));
4007
+ const failureError = childError && receiptOutput && receiptOutput !== childError
4008
+ ? `${childError}\n\n${receiptOutput}`
4009
+ : childError || receiptOutput || output || "Child run failed.";
3933
4010
  const detached = result.details.results.some((child) => child.detached);
3934
4011
  const interrupted = result.details.results.some((child) => child.interrupted);
3935
4012
  const stopped = result.details.results.some((child) => child.stopped);
@@ -3965,13 +4042,20 @@ function workflowChildResult(
3965
4042
  const continuationRunIds = [...new Set([resumeSourceRunId, runId].filter((value): value is string => Boolean(value)))];
3966
4043
  const outputReference = result.details.results.find((child) => child.savedOutputPath)?.savedOutputPath
3967
4044
  ?? result.details.results.find((child) => child.outputReference?.path)?.outputReference?.path;
4045
+ const externalResult = result.details.results.length === 1 && result.details.results[0]?.runner?.type === "external-cli" ? result.details.results[0] : undefined;
4046
+ const externalStatus = result.details.asyncDir ? readStatus(result.details.asyncDir) : undefined;
4047
+ const externalStatusStep = externalStatus?.steps?.length === 1 && externalStatus.steps[0]?.runner?.type === "external-cli" ? externalStatus.steps[0] : undefined;
4048
+ const externalRunner = normalizeExternalCliRunnerStatus(externalResult?.runner ?? externalStatusStep?.runner);
4049
+ const externalProcess = externalResult?.externalProcess ?? externalStatusStep?.externalProcess;
4050
+ const externalAdapter = externalRunner ? externalCliReceiptMetadata({ runner: externalRunner, externalProcess, outputReference }) : undefined;
4051
+ if (externalAdapter) resumability = { state: "not-resumable", reason: externalAdapter.nonResumableReason };
3968
4052
  return {
3969
4053
  key,
3970
4054
  ok,
3971
4055
  ...(resolvedAgents.length === 1 ? { agent: resolvedAgents[0] } : {}),
3972
4056
  ...(runId ? { runId } : {}),
3973
4057
  output,
3974
- ...(!ok ? { error: receiptOutput || output || "Child run failed." } : {}),
4058
+ ...(!ok ? { error: failureError } : {}),
3975
4059
  ...(detached ? { detached: true } : {}),
3976
4060
  ...(interrupted ? { interrupted: true } : {}),
3977
4061
  ...(stopped ? { stopped: true } : {}),
@@ -3979,6 +4063,7 @@ function workflowChildResult(
3979
4063
  ...(requestedContext ? { requestedContext } : {}),
3980
4064
  ...(resolvedContext ? { resolvedContext } : {}),
3981
4065
  ...(outputReference ? { outputReference } : {}),
4066
+ ...(externalAdapter ? { externalAdapter } : {}),
3982
4067
  resumability,
3983
4068
  continuation: { runIds: continuationRunIds },
3984
4069
  artifactPaths: [...artifactPaths],
@@ -4024,8 +4109,18 @@ function terminalWorkflowReceipt(
4024
4109
  workflowRunId: string,
4025
4110
  state: WorkflowReceiptState,
4026
4111
  children: WorkflowScriptChildResult[],
4112
+ workflowChildren?: WorkflowReceipt["workflowChildren"],
4027
4113
  ): WorkflowReceipt {
4028
- return buildWorkflowReceipt({ workflowRunId, state, children });
4114
+ return buildWorkflowReceipt({ workflowRunId, state, children, workflowChildren });
4115
+ }
4116
+
4117
+ function workflowFailureMessage(error: unknown, workflowRunId: string, children: WorkflowScriptChildResult[]): string {
4118
+ const text = error instanceof Error ? error.message : String(error);
4119
+ const validationPrefix = "workflowScript validation failed before child launch; no children launched.";
4120
+ if (children.length === 0 && text.includes(validationPrefix)) {
4121
+ return `Workflow '${workflowRunId}' validation failed before child launch; no children launched.${text.slice(text.indexOf(validationPrefix) + validationPrefix.length)}`;
4122
+ }
4123
+ return text;
4029
4124
  }
4030
4125
 
4031
4126
  export async function steerWorkflowChildByKey(input: {
@@ -4068,6 +4163,8 @@ export async function steerWorkflowChildByKey(input: {
4068
4163
  return { key: input.key, state: "missed", error: `Workflow child '${input.key}' is ${childStatus.state}.` };
4069
4164
  }
4070
4165
  if (childStatus) {
4166
+ const unsupported = externalRunnerControlError(asyncDir, "steer");
4167
+ if (unsupported) return workflowSteerReceipt(input.key, unsupported);
4071
4168
  const result = await steerAsyncRun({
4072
4169
  state: input.state,
4073
4170
  runId: childRunId,
@@ -4121,6 +4218,15 @@ export function prepareWorkflowLaunchParams(
4121
4218
  const toolBudget = childParams.toolBudget ?? workflowDefaults.toolBudget;
4122
4219
  const intercomBridge = childParams.intercomBridge ?? workflowDefaults.intercomBridge;
4123
4220
  const worktree = childParams.worktree ?? workflowDefaults.worktree;
4221
+ const outputSchema = Object.hasOwn(childParams, "outputSchema") ? childParams.outputSchema : workflowDefaults.outputSchema;
4222
+ if (outputSchema !== undefined) assertJsonSchemaObject(outputSchema, "outputSchema");
4223
+ const agentContract = Object.hasOwn(childParams, "agentContract") ? childParams.agentContract : workflowDefaults.agentContract;
4224
+ if (agentContract !== undefined && !isAgentContractV1(agentContract as AgentContract)) throw new Error("agentContract must be { version: 1 }.");
4225
+ const acceptance = Object.hasOwn(childParams, "acceptance") ? childParams.acceptance : workflowDefaults.acceptance;
4226
+ const output = Object.hasOwn(childParams, "output") ? childParams.output : workflowDefaults.output;
4227
+ if (output !== undefined && typeof output !== "string" && typeof output !== "boolean") throw new Error("output must be a path string or boolean.");
4228
+ const outputMode = Object.hasOwn(childParams, "outputMode") ? childParams.outputMode : workflowDefaults.outputMode;
4229
+ if (outputMode !== undefined && outputMode !== "inline" && outputMode !== "file-only") throw new Error("outputMode must be 'inline' or 'file-only'.");
4124
4230
  return {
4125
4231
  action: "resume",
4126
4232
  id: childParams.resume.trim(),
@@ -4129,6 +4235,11 @@ export function prepareWorkflowLaunchParams(
4129
4235
  workflowParentRunId: parentWorkflowRunId,
4130
4236
  workflowKey,
4131
4237
  ...(worktree !== undefined ? { worktree: worktree as boolean } : {}),
4238
+ ...(outputSchema !== undefined ? { outputSchema: outputSchema as JsonSchemaObject } : {}),
4239
+ ...(agentContract !== undefined ? { agentContract: agentContract as AgentContract } : {}),
4240
+ ...(acceptance !== undefined ? { acceptance: acceptance as AcceptanceInput } : {}),
4241
+ ...(output !== undefined ? { output: output as string | boolean } : {}),
4242
+ ...(outputMode !== undefined ? { outputMode: outputMode as OutputMode } : {}),
4132
4243
  ...(options.runFanoutBudget ? { runFanoutBudget: { ...options.runFanoutBudget, parentPath: `${options.runFanoutBudget.parentPath ? `${options.runFanoutBudget.parentPath}/` : ""}workflow[${workflowKey}]` } } : {}),
4133
4244
  ...(options.missionDetached ? { mission: false } : {}),
4134
4245
  ...(timeoutMs !== undefined ? { timeoutMs: timeoutMs as number } : {}),
@@ -4186,11 +4297,12 @@ function workflowChatProgressUpdate(
4186
4297
  runId: string,
4187
4298
  chatProgress: WorkflowChatProgressProjection,
4188
4299
  workflow: NonNullable<Details["workflow"]>,
4300
+ workflowChildren?: Details["workflowChildren"],
4189
4301
  ): AgentToolResult<Details> | undefined {
4190
4302
  if (chatProgress.mode !== "live-card") return undefined;
4191
4303
  return {
4192
4304
  content: [{ type: "text", text: "Workflow running." }],
4193
- details: { mode: "workflow", runId, results: [], workflow, chatProgress },
4305
+ details: { mode: "workflow", runId, results: [], workflow, ...(workflowChildren ? { workflowChildren } : {}), chatProgress },
4194
4306
  };
4195
4307
  }
4196
4308
 
@@ -4201,10 +4313,12 @@ function createScheduledOwnerState(source: SubagentState, ownerSessionId: string
4201
4313
  grantHistory: [...(source.subagentSpawns.grantHistory ?? [])],
4202
4314
  }
4203
4315
  : undefined;
4316
+ const ownerParentModel = source.currentSessionId === ownerSessionId ? source.lastParentModel : undefined;
4204
4317
  return {
4205
4318
  ...source,
4206
4319
  baseCwd: ctx.cwd,
4207
4320
  currentSessionId: ownerSessionId,
4321
+ lastParentModel: ownerParentModel,
4208
4322
  parentSessionFile: ctx.sessionManager.getSessionFile() ?? null,
4209
4323
  subagentInProgress: false,
4210
4324
  ...(ownerSpawns ? { subagentSpawns: ownerSpawns } : { subagentSpawns: undefined }),
@@ -4265,6 +4379,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4265
4379
  const delegatedThinkingOverrides = new WeakMap<object, AgentConfig["thinking"]>();
4266
4380
  const delegatedZeroToolBudgets = new WeakSet<object>();
4267
4381
  const delegatedExecutions = new WeakSet<object>();
4382
+ const workflowPermitContexts = new WeakMap<object, { root: WorkflowChildPermit } | { child: WorkflowChildPermitContext }>();
4268
4383
  const warnedArtifactPackageDirs = new Set<string>();
4269
4384
  const scheduledOwnerExecutors = new Map<string, ReturnType<typeof createSubagentExecutor>>();
4270
4385
  const execute = async (
@@ -4274,11 +4389,23 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4274
4389
  onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
4275
4390
  ctx: ExtensionContext,
4276
4391
  preserveActiveSession = false,
4392
+ parentModelOverride?: ParentModel | null,
4277
4393
  ): Promise<AgentToolResult<Details>> => {
4394
+ if (params.action?.trim() === "validate") {
4395
+ const validation = validateWorkflowScript(params.workflowScript ?? "");
4396
+ return {
4397
+ content: [{ type: "text", text: JSON.stringify(validation) }],
4398
+ ...(validation.ok ? {} : { isError: true }),
4399
+ details: { mode: "management", results: [] },
4400
+ };
4401
+ }
4278
4402
  const workflowLaunchObserver = workflowLaunchObservers.get(params);
4279
4403
  const delegatedThinkingOverride = delegatedThinkingOverrides.get(params);
4280
4404
  const allowZeroToolBudget = delegatedZeroToolBudgets.has(params);
4281
4405
  const delegatedExecution = delegatedExecutions.has(params);
4406
+ const workflowPermitContext = workflowPermitContexts.get(params);
4407
+ const delegatedWorkflowPermit = workflowPermitContext && "root" in workflowPermitContext ? workflowPermitContext.root : undefined;
4408
+ const workflowChildPermitLaunch = workflowPermitContext && "child" in workflowPermitContext ? workflowPermitContext.child : undefined;
4282
4409
  if (!preserveActiveSession) deps.state.baseCwd = ctx.cwd;
4283
4410
  deps.state.foregroundRuns ??= new Map();
4284
4411
  deps.state.foregroundControls ??= new Map();
@@ -4289,6 +4416,19 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4289
4416
  const normalizedAction = typeof requestParams.action === "string" ? requestParams.action.trim() : requestParams.action;
4290
4417
  if (normalizedAction === "resume" && requestParams.extensionBindings !== undefined) return buildRequestedModeError(requestParams, "extensionBindings is not supported with action='resume'; resume uses the original retained child binding.");
4291
4418
  if (requestParams.workflowScript !== undefined && normalizedAction === undefined) {
4419
+ if (delegatedWorkflowPermit) {
4420
+ const permitError = validateWorkflowChildPermitRoot(delegatedWorkflowPermit, _id);
4421
+ if (permitError) return buildRequestedModeError(requestParams, permitError);
4422
+ if (requestParams.async !== false) return buildRequestedModeError(requestParams, "Workflow child permit supports foreground workflow roots only; set async:false.");
4423
+ }
4424
+ const workflowParentModel = parentModelOverride !== undefined
4425
+ ? parentModelOverride
4426
+ : (() => {
4427
+ const currentParentModel = normalizeParentModel(ctx.model);
4428
+ return (preserveActiveSession
4429
+ ? currentParentModel
4430
+ : rememberParentModel(deps.state, resolveCurrentSessionId(ctx.sessionManager), currentParentModel)) ?? null;
4431
+ })();
4292
4432
  if (requestParams.extensionBindings !== undefined) {
4293
4433
  try {
4294
4434
  requestParams.extensionBindings = normalizeExtensionBindings(requestParams.extensionBindings)!.value;
@@ -4340,7 +4480,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4340
4480
  runId: workflowRunId,
4341
4481
  kind: "workflow",
4342
4482
  asyncDir: path.join(DIRS.async, workflowRunId),
4343
- }, { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) });
4483
+ }, { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(deps.config.capacity?.abandonedSlotReleaseAfterMs) });
4344
4484
  } catch (error) {
4345
4485
  if (error instanceof ActiveAsyncCapacityError) {
4346
4486
  deps.state.activeAsyncCapacity = error.snapshot;
@@ -4414,7 +4554,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4414
4554
  deps.state.workflowChildStops ??= new Map();
4415
4555
  deps.state.workflowControllers.set(workflowRunId, controller);
4416
4556
  workflowCapacity?.markWorkflowStarted();
4417
- if (workflowCapacity) deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(currentSessionId, resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession), { liveWorkflowRunIds: new Set(deps.state.workflowControllers.keys()) });
4557
+ if (workflowCapacity) deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(currentSessionId, resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession), { liveWorkflowRunIds: new Set(deps.state.workflowControllers.keys()), abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(deps.config.capacity?.abandonedSlotReleaseAfterMs) });
4418
4558
  let status: AsyncStatus = {
4419
4559
  runId: workflowRunId,
4420
4560
  toolCallId,
@@ -4431,6 +4571,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4431
4571
  pid: process.pid,
4432
4572
  steps: [],
4433
4573
  workflow: { trace: [], emits: [], console: [] },
4574
+ workflowChildren: workflowChildSummary({ parentToolCallId: toolCallId, workflowRunId, workflowState: "running", inventoryComplete: false }),
4434
4575
  runFanoutBudget: getRunFanoutBudgetSnapshot(workflowFanoutBudget),
4435
4576
  };
4436
4577
  const appendWorkflowEvent = (event: Record<string, unknown>) => {
@@ -4479,6 +4620,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4479
4620
  if (persistClosed) return;
4480
4621
  const liveJob = deps.state.asyncJobs.get(workflowRunId);
4481
4622
  if (liveJob && (liveJob.status === "complete" || liveJob.status === "failed") && status.state !== "complete" && status.state !== "failed") return;
4623
+ const workflowState = status.state === "complete" ? "completed" : status.state === "failed" || status.state === "rejected" ? "failed" : status.state === "paused" ? "paused" : status.state === "stopped" ? "stopped" : "running";
4624
+ status.workflowChildren = workflowChildSummary({ parentToolCallId: toolCallId, workflowRunId, workflowState, inventoryComplete: workflowState !== "running", trace: status.workflow?.trace, steps: status.steps });
4482
4625
  status.lastUpdate = Date.now();
4483
4626
  if (!initialPersistenceComplete) {
4484
4627
  writeAtomicJson(statusPath, status);
@@ -4522,6 +4665,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4522
4665
  delete liveJob.agents;
4523
4666
  }
4524
4667
  liveJob.workflow = status.workflow;
4668
+ liveJob.workflowChildren = status.workflowChildren;
4525
4669
  }
4526
4670
  };
4527
4671
  const writeWorkflowResult = (payload: Record<string, unknown>): boolean => {
@@ -4556,7 +4700,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4556
4700
  status.toolCount = toolCounts.length > 0 ? toolCounts.reduce((total, count) => total + count, 0) : undefined;
4557
4701
  status.currentStep = runningSteps.length === 1 ? steps.indexOf(runningSteps[0]!) : undefined;
4558
4702
  };
4559
- const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, toolCallId, cwd: workflowCwd, ...(workflowSessionRoot ? { sessionRoot: workflowSessionRoot } : {}), status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
4703
+ const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, toolCallId, cwd: workflowCwd, ...(workflowSessionRoot ? { sessionRoot: workflowSessionRoot } : {}), status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow, workflowChildren: status.workflowChildren };
4560
4704
  deps.state.asyncJobs.set(workflowRunId, workflowJob);
4561
4705
  deps.state.fleetJobs ??= new Map();
4562
4706
  deps.state.fleetJobs.set(workflowRunId, workflowJob);
@@ -4743,7 +4887,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4743
4887
  status: step.status,
4744
4888
  heartbeat: { status: step.status, ...(childPhase ? { phase: childPhase } : {}) },
4745
4889
  });
4746
- }, ctx, preserveActiveSession);
4890
+ }, ctx, preserveActiveSession, workflowParentModel);
4747
4891
  });
4748
4892
  workflowResults.push(...result.details.results);
4749
4893
  for (const childResult of result.details.results) {
@@ -4773,7 +4917,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4773
4917
  }
4774
4918
  return child;
4775
4919
  },
4776
- status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession)),
4920
+ status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession, workflowParentModel)),
4777
4921
  resolveResume: (reference) => resolveKeyedWorkflowResume(reference, deps.state),
4778
4922
  steer: (key, message, options, workflowSignal) => steerWorkflowChildByKey({ state: deps.state, workflowRunId, key, message, options, signal: workflowSignal, resolveRunId: () => workflowChildRunIds.get(key) }),
4779
4923
  });
@@ -4783,15 +4927,16 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4783
4927
  const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, summary, producedChildOutputPaths);
4784
4928
  const resultSummary = appendWorkflowOutputWarning(summary, outputWarning);
4785
4929
  const workflowUsage = sumResultsUsage(workflowResults);
4786
- status = { ...status, state: "complete", endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, totalTokens: { input: workflowUsage.input, output: workflowUsage.output, total: workflowUsage.input + workflowUsage.output }, totalCost: sumResultsCost(workflowResults) };
4787
- const receipt = terminalWorkflowReceipt(workflowRunId, "complete", workflow.children);
4930
+ const workflowChildren = workflowChildSummary({ parentToolCallId: toolCallId, workflowRunId, workflowState: "completed", inventoryComplete: true, trace: workflow.trace, children: workflow.children, steps: status.steps });
4931
+ status = { ...status, state: "complete", endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, workflowChildren, totalTokens: { input: workflowUsage.input, output: workflowUsage.output, total: workflowUsage.input + workflowUsage.output }, totalCost: sumResultsCost(workflowResults) };
4932
+ const receipt = terminalWorkflowReceipt(workflowRunId, "complete", workflow.children, workflowChildren);
4788
4933
  let workflowReceipt: { path: string; receipt: WorkflowReceipt } | undefined;
4789
4934
  try {
4790
4935
  workflowReceipt = { path: writeWorkflowReceipt(asyncDir, receipt), receipt };
4791
4936
  } catch (receiptError) {
4792
4937
  appendWorkflowEvent({ type: "subagent.workflow.receipt_write_failed", error: `Failed to persist async workflow receipt: ${receiptError instanceof Error ? receiptError.message : String(receiptError)}` });
4793
4938
  }
4794
- if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary: resultSummary, output: resultSummary, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.stopped ? { stopped: true } : {}), ...(child.interrupted ? { interrupted: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
4939
+ if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary: resultSummary, output: resultSummary, workflowChildren, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.stopped ? { stopped: true } : {}), ...(child.interrupted ? { interrupted: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
4795
4940
  persist();
4796
4941
  persistClosed = true;
4797
4942
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}) });
@@ -4813,7 +4958,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4813
4958
  delete step.activityState;
4814
4959
  }
4815
4960
  }
4816
- status = compactOptional<AsyncStatus>({ ...status, state, stopped: stopped || undefined, activityState: pauseForDetached ? "needs_attention" : undefined, error: error instanceof Error ? error.message : String(error), endedAt: Date.now(), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console } });
4961
+ const workflowState = state === "paused" ? "paused" : state === "stopped" ? "stopped" : "failed";
4962
+ const workflowChildren = workflowChildSummary({ parentToolCallId: toolCallId, workflowRunId, workflowState, inventoryComplete: true, trace: partial.trace, children: partial.children, steps: status.steps });
4963
+ status = compactOptional<AsyncStatus>({ ...status, state, stopped: stopped || undefined, activityState: pauseForDetached ? "needs_attention" : undefined, error: workflowFailureMessage(error, workflowRunId, partial.children), endedAt: Date.now(), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console }, workflowChildren });
4817
4964
  if (pauseForDetached) {
4818
4965
  const promoted = promotePausedWorkflowIfSettled(status);
4819
4966
  if (promoted) status = promoted;
@@ -4824,14 +4971,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4824
4971
  const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, terminalSummary, producedChildOutputPaths);
4825
4972
  const resultSummary = appendWorkflowOutputWarning(terminalSummary, outputWarning);
4826
4973
  const receiptState: WorkflowReceiptState = status.state === "complete" ? "complete" : status.state === "paused" ? "paused" : status.state === "stopped" ? "stopped" : "failed";
4827
- const receipt = terminalWorkflowReceipt(workflowRunId, receiptState, partial.children);
4974
+ const receipt = terminalWorkflowReceipt(workflowRunId, receiptState, partial.children, workflowChildren);
4828
4975
  let workflowReceipt: { path: string; receipt: WorkflowReceipt } | undefined;
4829
4976
  try {
4830
4977
  workflowReceipt = { path: writeWorkflowReceipt(asyncDir, receipt), receipt };
4831
4978
  } catch (receiptError) {
4832
4979
  appendWorkflowEvent({ type: "subagent.workflow.receipt_write_failed", error: `Failed to persist async workflow receipt: ${receiptError instanceof Error ? receiptError.message : String(receiptError)}` });
4833
4980
  }
4834
- if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: status.state === "complete", state: status.state, summary: resultSummary, error: status.state === "complete" ? undefined : status.error, stopped: status.stopped, activityState: status.activityState, results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.stopped ? { stopped: true } : {}), ...(child.interrupted ? { interrupted: true } : {}), ...(child.detached && status.state !== "complete" ? { detached: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
4981
+ if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: status.state === "complete", state: status.state, summary: resultSummary, error: status.state === "complete" ? undefined : status.error, stopped: status.stopped, activityState: status.activityState, workflowChildren, results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.stopped ? { stopped: true } : {}), ...(child.interrupted ? { interrupted: true } : {}), ...(child.detached && status.state !== "complete" ? { detached: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, ...(workflowReceipt ? { workflowReceipt } : {}), asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
4835
4982
  persist();
4836
4983
  persistClosed = true;
4837
4984
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}), ...(status.activityState ? { activityState: status.activityState } : {}) });
@@ -4845,7 +4992,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4845
4992
  });
4846
4993
  return attachWorkflowMission(withRunFanoutBudget({
4847
4994
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async workflow [${workflowRunId}]`, ctx.hasUI === true) }],
4848
- details: { mode: "workflow", runId: workflowRunId, toolCallId, asyncId: workflowRunId, asyncDir, results: [], chatProgress, ...(deps.state.activeAsyncCapacity ? { activeAsyncCapacity: deps.state.activeAsyncCapacity } : {}) },
4995
+ details: { mode: "workflow", runId: workflowRunId, toolCallId, asyncId: workflowRunId, asyncDir, results: [], workflowChildren: status.workflowChildren, chatProgress, ...(deps.state.activeAsyncCapacity ? { activeAsyncCapacity: deps.state.activeAsyncCapacity } : {}) },
4849
4996
  }, workflowFanoutBudget));
4850
4997
  }
4851
4998
  const { workflowScript: _workflowScript, action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, async: _async, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, chatProgress: _chatProgress, missionId: _missionId, mission: _mission, ...workflowChildDefaults } = requestParams;
@@ -4858,20 +5005,23 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4858
5005
  const workflowResults: SingleResult[] = [];
4859
5006
  const workflowChildRunIds = new Map<string, string>();
4860
5007
  let liveWorkflow: NonNullable<Details["workflow"]> = { trace: [], emits: [], console: [] };
5008
+ let liveWorkflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: _id, workflowState: "running", inventoryComplete: false });
4861
5009
  const workflowDeadlineAt = timeout === undefined ? undefined : Date.now() + timeout;
4862
5010
  const workflowCapabilityCeiling = intersectSubagentCapabilityCeilings(requestParams.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(resolveCurrentSessionId(ctx.sessionManager)));
4863
5011
  const sendWorkflowProgress = () => {
4864
- const update = workflowChatProgressUpdate(_id, chatProgress, liveWorkflow);
5012
+ const update = workflowChatProgressUpdate(_id, chatProgress, liveWorkflow, liveWorkflowChildren);
4865
5013
  if (update) onUpdate?.(update);
4866
5014
  };
4867
5015
  try {
4868
5016
  const workflow = await runWorkflowScript({
4869
5017
  script: requestParams.workflowScript,
5018
+ ...(delegatedWorkflowPermit ? { oneUsePermit: { claim: (key: string) => claimWorkflowChildPermit(delegatedWorkflowPermit, _id, key) } } : {}),
4870
5019
  timeoutMs: timeout,
4871
5020
  signal,
4872
5021
  ...(workflowState ? { state: workflowState } : {}),
4873
5022
  onTrace: (trace) => {
4874
5023
  liveWorkflow = { ...liveWorkflow, trace };
5024
+ liveWorkflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: _id, workflowState: "running", inventoryComplete: false, trace });
4875
5025
  sendWorkflowProgress();
4876
5026
  },
4877
5027
  admit: (calls) => {
@@ -4886,6 +5036,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4886
5036
  sendWorkflowProgress();
4887
5037
  },
4888
5038
  launch: async (key, childParams, workflowSignal, admission) => {
5039
+ if (delegatedWorkflowPermit && admission.batch) throw new Error("Workflow child permit does not support runs.all.");
5040
+ if (delegatedWorkflowPermit && childParams.resume !== undefined) throw new Error("Workflow child permit does not support retained resume.");
5041
+ if (delegatedWorkflowPermit && childParams.async === true) throw new Error("Workflow child permit supports one foreground child only.");
4889
5042
  if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."), childParams, deps.state);
4890
5043
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4891
5044
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)), childParams, deps.state);
@@ -4904,6 +5057,15 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4904
5057
  missionBinding,
4905
5058
  deps.asyncByDefault,
4906
5059
  );
5060
+ if (delegatedWorkflowPermit) {
5061
+ if (childRequest.async !== false) throw new Error("Workflow child permit supports one foreground child only.");
5062
+ const childCwd = resolveRequestedCwd(workflowCwd, childRequest.cwd);
5063
+ const childAgent = typeof childRequest.agent === "string"
5064
+ ? resolveAgentName(childRequest.agent, deps.discoverAgents(childCwd, resolveExecutionAgentScope(childRequest.agentScope)).agents).agent
5065
+ : undefined;
5066
+ if (childAgent?.runner?.type === "external-cli" || childAgent?.runner?.type === "external-job") throw new Error("Workflow child permit supports native Pi children only.");
5067
+ workflowPermitContexts.set(childRequest, { child: { permit: delegatedWorkflowPermit, workflowRunId: _id, childKey: key } });
5068
+ }
4907
5069
  workflowLaunchObservers.set(childRequest, (launch) => recordMissionWorkflowChild(missionBinding, _id, key, {
4908
5070
  status: "running",
4909
5071
  agent: launch.agent,
@@ -4917,7 +5079,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4917
5079
  status: progressStatus,
4918
5080
  heartbeat: { status: progressStatus, ...(childPhase ? { phase: childPhase } : {}) },
4919
5081
  });
4920
- }, ctx, preserveActiveSession);
5082
+ }, ctx, preserveActiveSession, workflowParentModel);
4921
5083
  });
4922
5084
  workflowResults.push(...result.details.results);
4923
5085
  for (const childResult of result.details.results) {
@@ -4938,11 +5100,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4938
5100
  });
4939
5101
  return child;
4940
5102
  },
4941
- status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession)),
5103
+ status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession, workflowParentModel)),
4942
5104
  resolveResume: (reference) => resolveKeyedWorkflowResume(reference, deps.state),
4943
5105
  steer: (key, message, options, workflowSignal) => steerWorkflowChildByKey({ state: deps.state, workflowRunId: _id, key, message, options, signal: workflowSignal, resolveRunId: () => workflowChildRunIds.get(key) }),
4944
5106
  });
4945
- const receipt = terminalWorkflowReceipt(_id, "complete", workflow.children);
5107
+ const workflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: _id, workflowState: "completed", inventoryComplete: true, trace: workflow.trace, children: workflow.children });
5108
+ const receipt = terminalWorkflowReceipt(_id, "complete", workflow.children, workflowChildren);
4946
5109
  const traceLines = workflow.trace.map((entry) => `- ${entry.operation} ${entry.key}: ${entry.state}${entry.runId ? ` (${entry.runId})` : ""}${entry.durationMs !== undefined ? ` in ${entry.durationMs}ms` : ""}${entry.error ? ` — ${entry.error}` : ""}`);
4947
5110
  const sections = ["Workflow completed.", `Return:\n${formatWorkflowValue(workflow.value)}`];
4948
5111
  if (workflow.emits.length > 0) sections.push(`Emitted:\n${workflow.emits.map(formatWorkflowValue).join("\n")}`);
@@ -4953,11 +5116,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4953
5116
  const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
4954
5117
  return attachWorkflowMission(withRunFanoutBudget({
4955
5118
  content: [{ type: "text", text: displayText }],
4956
- details: compactOptional<Details>({ mode: "workflow", runId: _id, results: workflow.children.flatMap((child) => (child.results ?? []) as SingleResult[]), totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console, receipt }, chatProgress }),
5119
+ details: compactOptional<Details>({ mode: "workflow", runId: _id, results: workflow.children.flatMap((child) => (child.results ?? []) as SingleResult[]), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console, receipt }, chatProgress }),
4957
5120
  }, workflowFanoutBudget));
4958
5121
  } catch (error) {
4959
5122
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
4960
- const text = error instanceof Error ? error.message : String(error);
5123
+ const text = workflowFailureMessage(error, _id, partial.children);
4961
5124
  const traceLines = partial.trace.map((entry) => `- ${entry.operation} ${entry.key}: ${entry.state}${entry.runId ? ` (${entry.runId})` : ""}${entry.error ? ` — ${entry.error}` : ""}`);
4962
5125
  const sections = [`Workflow failed: ${text}`];
4963
5126
  if (partial.emits.length > 0) sections.push(`Emitted:\n${partial.emits.map(formatWorkflowValue).join("\n")}`);
@@ -4966,11 +5129,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4966
5129
  const workflowText = sections.join("\n\n");
4967
5130
  const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText, producedChildOutputPaths);
4968
5131
  const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
4969
- const receipt = terminalWorkflowReceipt(_id, "failed", partial.children);
5132
+ const workflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: _id, workflowState: "failed", inventoryComplete: true, trace: partial.trace, children: partial.children });
5133
+ const receipt = terminalWorkflowReceipt(_id, "failed", partial.children, workflowChildren);
4970
5134
  return attachWorkflowMission(withRunFanoutBudget({
4971
5135
  content: [{ type: "text", text: displayText }],
4972
5136
  isError: true,
4973
- details: compactOptional<Details>({ mode: "workflow", runId: _id, results: partial.children.flatMap((child) => (child.results ?? []) as SingleResult[]), totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console, receipt }, chatProgress }),
5137
+ details: compactOptional<Details>({ mode: "workflow", runId: _id, results: partial.children.flatMap((child) => (child.results ?? []) as SingleResult[]), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console, receipt }, chatProgress }),
4974
5138
  }, workflowFanoutBudget));
4975
5139
  }
4976
5140
  }
@@ -4984,9 +5148,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4984
5148
  try {
4985
5149
  requestSessionId = resolveCurrentSessionId(ctx.sessionManager);
4986
5150
  requestPiSessionId = ctx.sessionManager.getSessionId() ?? undefined;
4987
- requestParentModel = preserveActiveSession
4988
- ? normalizeParentModel(ctx.model)
4989
- : rememberParentModel(deps.state, requestSessionId, ctx.model);
5151
+ requestParentModel = parentModelOverride !== undefined
5152
+ ? parentModelOverride ?? undefined
5153
+ : preserveActiveSession
5154
+ ? normalizeParentModel(ctx.model)
5155
+ : rememberParentModel(deps.state, requestSessionId, ctx.model);
4990
5156
  } catch (error) {
4991
5157
  if (action?.toLowerCase() !== "doctor" && action?.toLowerCase() !== "guide") throw error;
4992
5158
  requestParentModel = normalizeParentModel(ctx.model);
@@ -5211,7 +5377,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5211
5377
  }
5212
5378
  const spawnBudget = getSpawnBudgetSnapshot(deps.state, deps.config, currentSessionId);
5213
5379
  const activeAsyncCapacity = currentSessionId
5214
- ? getActiveAsyncCapacitySnapshot(currentSessionId, resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession), { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) })
5380
+ ? getActiveAsyncCapacitySnapshot(currentSessionId, resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession), { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(deps.config.capacity?.abandonedSlotReleaseAfterMs) })
5215
5381
  : { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession) ?? 0 };
5216
5382
  deps.state.activeAsyncCapacity = activeAsyncCapacity;
5217
5383
  return {
@@ -5256,10 +5422,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5256
5422
  if (paramsWithResolvedCwd.view) {
5257
5423
  return withBudget({ content: [{ type: "text", text: "action='debug.run' does not support status views." }], isError: true, details: { mode: "management", results: [] } });
5258
5424
  }
5259
- return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots })));
5425
+ return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots, abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(deps.config.capacity?.abandonedSlotReleaseAfterMs) })));
5260
5426
  }
5261
5427
  if (paramsWithResolvedCwd.view === "fleet") {
5262
- return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots })));
5428
+ return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots, abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(deps.config.capacity?.abandonedSlotReleaseAfterMs) })));
5263
5429
  }
5264
5430
  if (targetRunId) {
5265
5431
  try {
@@ -5290,7 +5456,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5290
5456
  });
5291
5457
  }
5292
5458
  }
5293
- return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots })));
5459
+ return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots, abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(deps.config.capacity?.abandonedSlotReleaseAfterMs) })));
5294
5460
  }
5295
5461
  if (action === "resume") {
5296
5462
  return resumeAsyncRun(omitUndefinedProperties({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel, signal }));
@@ -5616,7 +5782,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5616
5782
  const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
5617
5783
  const discovered = deps.discoverAgents(effectiveCwd, scope);
5618
5784
  const discoveredAgents = discovered.agents;
5619
- const canonicalParams = canonicalizeExecutionParams(effectiveParams, discoveredAgents, discovered.agentDiagnostics);
5785
+ const unknownAgentDiagnosticContext = diagnosticContextFromDiscovery(discovered, effectiveCwd, scope);
5786
+ const canonicalParams = canonicalizeExecutionParams(effectiveParams, discoveredAgents, discovered.agentDiagnostics, unknownAgentDiagnosticContext);
5620
5787
  if (canonicalParams.error) return buildRequestedModeError(effectiveParams, canonicalParams.error);
5621
5788
  effectiveParams = canonicalParams.params!;
5622
5789
  const modelScope = discovered.modelScope;
@@ -5665,6 +5832,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5665
5832
  hasTasks,
5666
5833
  hasSingle,
5667
5834
  allowClarifyTaskPrompt,
5835
+ unknownAgentDiagnosticContext,
5668
5836
  );
5669
5837
  if (validationError) return validationError;
5670
5838
 
@@ -5679,6 +5847,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5679
5847
  if (spawnPreflight.error) return spawnBudgetErrorResult(spawnPreflight.error, foregroundMode);
5680
5848
 
5681
5849
  let forkSessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
5850
+ let prepareForkSessionForIndex: (idx?: number) => Promise<void> = async () => {};
5682
5851
  let forkThinkingOverrideForIndex: (idx?: number) => AgentConfig["thinking"] | undefined = () => undefined;
5683
5852
  let prepareForkThinking = (_agentName: string, _index: number, _modelOverride?: string, _modelOverrideFromParent?: boolean): void => {};
5684
5853
  const forkThinkingRequirements = new Map<number, boolean>();
@@ -5714,9 +5883,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5714
5883
  || candidates.some((candidate) => forkedChildRequiresThinkingOff(candidate, forkAvailableModels, parentModel?.provider)),
5715
5884
  );
5716
5885
  };
5886
+ const pruneSession = contextPolicy.usesFork && deps.config.forkContext?.mode === "pruned"
5887
+ ? await createPrunedForkSessionWriter(ctx, deps.config.forkContext, signal)
5888
+ : undefined;
5717
5889
  const forkContextResolver = createForkContextResolver(ctx.sessionManager, contextPolicy.usesFork ? "fork" : undefined, {
5718
5890
  forceThinkingOffForIndex: (index) => forkThinkingRequirements.get(index) ?? true,
5891
+ ...(pruneSession ? { pruneSession } : {}),
5719
5892
  });
5893
+ prepareForkSessionForIndex = forkContextResolver.prepareSessionForIndex;
5720
5894
  forkSessionFileForIndex = forkContextResolver.sessionFileForIndex;
5721
5895
  forkThinkingOverrideForIndex = forkContextResolver.thinkingOverrideForIndex;
5722
5896
  } catch (error) {
@@ -5760,8 +5934,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5760
5934
  runId: asyncRunId,
5761
5935
  kind: "runner",
5762
5936
  asyncDir: path.join(DIRS.async, asyncRunId),
5763
- }, { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) });
5764
- deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(requestSessionId, activeLimit, { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) });
5937
+ }, { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(deps.config.capacity?.abandonedSlotReleaseAfterMs) });
5938
+ deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(requestSessionId, activeLimit, { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(deps.config.capacity?.abandonedSlotReleaseAfterMs) });
5765
5939
  } catch (error) {
5766
5940
  if (error instanceof ActiveAsyncCapacityError) {
5767
5941
  deps.state.activeAsyncCapacity = error.snapshot;
@@ -5824,6 +5998,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5824
5998
  prepareForkThinking(agentName, idx, modelOverride, modelOverrideFromParent);
5825
5999
  return forkSessionFileForIndex(idx);
5826
6000
  };
6001
+ const prepareForkSessionForTask: PrepareForkSessionForTask = async (agentName, idx = 0, modelOverride, modelOverrideFromParent) => {
6002
+ if (!shouldForkAgent(contextPolicy, agentName)) return;
6003
+ prepareForkThinking(agentName, idx, modelOverride, modelOverrideFromParent);
6004
+ await prepareForkSessionForIndex(idx);
6005
+ };
5827
6006
  const forkThinkingOverrideForTask: ForkThinkingOverrideForTask = (agentName, idx = 0, modelOverride, modelOverrideFromParent) => {
5828
6007
  if (!shouldForkAgent(contextPolicy, agentName)) return delegatedThinkingOverride;
5829
6008
  prepareForkThinking(agentName, idx, modelOverride, modelOverrideFromParent);
@@ -5836,8 +6015,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5836
6015
  const childSessionFileForIndex = (idx?: number) =>
5837
6016
  path.join(sessionDirForIndex(idx), "session.jsonl");
5838
6017
  try {
5839
- if (!(effectiveParams.clarify === true && ctx.hasUI)) {
5840
- preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems);
6018
+ if (!(effectiveParams.clarify === true && ctx.hasUI) || deps.config.forkContext?.mode === "pruned") {
6019
+ await preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, prepareForkSessionForTask, deps.config.chain?.dynamicFanout?.maxItems);
5841
6020
  }
5842
6021
  } catch (error) {
5843
6022
  activeAsyncCapacity?.rollback();
@@ -5910,6 +6089,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5910
6089
  signal,
5911
6090
  onUpdate: onUpdateWithContext,
5912
6091
  agents,
6092
+ unknownAgentDiagnosticContext,
5913
6093
  recoveryAgents: discoveredAgents,
5914
6094
  runId,
5915
6095
  shareEnabled,
@@ -5942,6 +6122,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5942
6122
  runFanoutBudget,
5943
6123
  topLevelAsyncCapacityEligible,
5944
6124
  activeAsyncCapacity,
6125
+ workflowChildPermitLaunch,
5945
6126
  });
5946
6127
 
5947
6128
  const foregroundDescription = selectedAgentNames.length === 1
@@ -6192,7 +6373,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6192
6373
  if (!normalized.ok) {
6193
6374
  return Promise.resolve({ content: [{ type: "text", text: normalized.error }], isError: true, details: { mode: normalized.mode, results: [] } });
6194
6375
  }
6195
- return executeWithSingleDispatchGuard(id, normalized.params, signal, onUpdate, ctx);
6376
+ const loaded = loadWorkflowScriptPath(normalized.params, ctx.cwd);
6377
+ if (loaded.error) {
6378
+ return Promise.resolve({ content: [{ type: "text", text: loaded.error }], isError: true, details: { mode: normalized.params.action ? "management" : "workflow", results: [] } });
6379
+ }
6380
+ return executeWithSingleDispatchGuard(id, loaded.params!, signal, onUpdate, ctx);
6196
6381
  };
6197
6382
 
6198
6383
  const executeDelegated = async (
@@ -6206,13 +6391,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6206
6391
  const privateParams = delegatedParams as SubagentParamsLike & {
6207
6392
  delegatedThinkingOverride?: AgentConfig["thinking"];
6208
6393
  delegatedAllowZeroToolBudget?: true;
6394
+ delegatedWorkflowPermit?: WorkflowChildPermit;
6209
6395
  };
6210
6396
  const thinkingOverride = privateParams.delegatedThinkingOverride;
6211
6397
  const allowZeroToolBudget = privateParams.delegatedAllowZeroToolBudget === true;
6398
+ const workflowPermit = privateParams.delegatedWorkflowPermit;
6212
6399
  delete privateParams.delegatedThinkingOverride;
6213
6400
  delete privateParams.delegatedAllowZeroToolBudget;
6401
+ delete privateParams.delegatedWorkflowPermit;
6214
6402
  if (thinkingOverride !== undefined) delegatedThinkingOverrides.set(delegatedParams, thinkingOverride);
6215
6403
  if (allowZeroToolBudget) delegatedZeroToolBudgets.add(delegatedParams);
6404
+ if (workflowPermit) workflowPermitContexts.set(delegatedParams, { root: workflowPermit });
6216
6405
  delegatedExecutions.add(delegatedParams);
6217
6406
  return execute(id, delegatedParams, signal, onUpdate, ctx);
6218
6407
  };