pi-subagents 0.57.0 → 0.58.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 (55) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/docs/agents.md +13 -6
  3. package/docs/extension-api.md +39 -0
  4. package/docs/models.md +3 -3
  5. package/docs/tool-reference.md +3 -2
  6. package/docs/workflows.md +24 -0
  7. package/package.json +1 -1
  8. package/skills/pi-subagents/SKILL.md +2 -0
  9. package/skills/pi-subagents/references/execution-controls.md +1 -1
  10. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -0
  11. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  12. package/src/agents/agent-management.ts +23 -0
  13. package/src/agents/agent-serializer.ts +5 -0
  14. package/src/agents/agents.ts +38 -3
  15. package/src/agents/runtime-agent-events.ts +70 -0
  16. package/src/agents/runtime-agent-registry.ts +10 -2
  17. package/src/api/agents.ts +10 -5
  18. package/src/api/preflight.ts +26 -1
  19. package/src/extension/index.ts +15 -4
  20. package/src/extension/rpc.ts +35 -14
  21. package/src/extension/schemas.ts +2 -2
  22. package/src/extension/tool-description.ts +8 -4
  23. package/src/integrations/herdr-status.ts +5 -0
  24. package/src/runs/background/async-execution.ts +29 -12
  25. package/src/runs/background/async-resume.ts +6 -2
  26. package/src/runs/background/async-status.ts +2 -0
  27. package/src/runs/background/chain-append.ts +2 -0
  28. package/src/runs/background/notify.ts +26 -3
  29. package/src/runs/background/result-delivery-ownership.ts +45 -0
  30. package/src/runs/background/result-watcher.ts +27 -10
  31. package/src/runs/background/subagent-runner.ts +71 -21
  32. package/src/runs/foreground/execution.ts +20 -1
  33. package/src/runs/foreground/subagent-executor.ts +57 -26
  34. package/src/runs/foreground/workflow-detach-reconcile.ts +128 -17
  35. package/src/runs/shared/completion-guard.ts +4 -3
  36. package/src/runs/shared/dynamic-fanout.ts +3 -3
  37. package/src/runs/shared/fast-mode-extension.ts +5 -5
  38. package/src/runs/shared/launch-cwd.ts +16 -0
  39. package/src/runs/shared/long-running-guard.ts +2 -1
  40. package/src/runs/shared/mcp-config-sources.ts +386 -0
  41. package/src/runs/shared/mcp-direct-tool-allowlist.ts +155 -42
  42. package/src/runs/shared/model-exclusions.ts +12 -0
  43. package/src/runs/shared/model-fallback.ts +16 -4
  44. package/src/runs/shared/parallel-utils.ts +8 -1
  45. package/src/runs/shared/pi-args.ts +61 -6
  46. package/src/runs/shared/single-output.ts +17 -0
  47. package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
  48. package/src/shared/formatters.ts +6 -0
  49. package/src/shared/launch-contract.ts +4 -0
  50. package/src/shared/types.ts +42 -1
  51. package/src/shared/utils.ts +6 -29
  52. package/src/slash/subagents-admin.ts +3 -0
  53. package/src/tui/render.ts +14 -6
  54. package/src/workflows/scripted-workflow.ts +52 -5
  55. package/src/workflows/workflow-receipt.ts +39 -4
@@ -75,7 +75,7 @@ import { usageBudgetExceededMessage, usageBudgetState, validateUsageBudgetConfig
75
75
  import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
76
76
  import { isAgentContractV1 } from "../shared/agent-contract.ts";
77
77
  import { normalizeExtensionBindings, type ExtensionBindings } from "../shared/extension-bindings.ts";
78
- import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
78
+ import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, outputPathMappingFromTask, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
79
79
  import { assertJsonSchemaObject, cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-output.ts";
80
80
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
81
81
  import { createTaskMutationArbiter } from "../shared/llm-intent-arbiter.ts";
@@ -169,6 +169,7 @@ import {
169
169
  type ToolBudgetConfig,
170
170
  type TurnBudgetConfig,
171
171
  type UsageBudgetConfig,
172
+ type WorkflowTerminalOutcome,
172
173
  type SubagentRunMode,
173
174
  type SubagentState,
174
175
  DIRS,
@@ -418,6 +419,7 @@ type ForkThinkingOverrideForTask = (agentName: string, idx?: number, modelOverri
418
419
  interface ExecutionContextData {
419
420
  params: SubagentParamsLike;
420
421
  effectiveCwd: string;
422
+ requestedCwd?: string;
421
423
  ctx: ExtensionContext;
422
424
  signal: AbortSignal;
423
425
  onUpdate?: (r: AgentToolResult<Details>) => void;
@@ -1793,6 +1795,7 @@ async function resumeAsyncRun(input: {
1793
1795
  systemPrompt: "",
1794
1796
  systemPromptMode: recoveryDescriptor.systemPromptMode,
1795
1797
  inheritProjectContext: recoveryDescriptor.inheritProjectContext,
1798
+ inheritGlobalContext: recoveryDescriptor.inheritGlobalContext,
1796
1799
  inheritSkills: recoveryDescriptor.inheritSkills,
1797
1800
  source: "project",
1798
1801
  filePath: recoveryDescriptor.agentFilePath ?? path.join(getProjectSubagentsDir(recoveryDescriptor.cwd), "recovery-agent"),
@@ -3237,6 +3240,7 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
3237
3240
  ctx: asyncCtx,
3238
3241
  availableModels,
3239
3242
  cwd: effectiveCwd,
3243
+ requestedCwd: data.requestedCwd,
3240
3244
  maxOutput: params.maxOutput,
3241
3245
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3242
3246
  artifactConfig,
@@ -3701,6 +3705,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3701
3705
  try {
3702
3706
  r = await runSync(ctx.cwd, agents, params.agent!, task, compactOptional<Parameters<typeof runSync>[4]>({
3703
3707
  permissions: deps.config.permissions,
3708
+ runtimeSnapshotHost: deps.pi,
3704
3709
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
3705
3710
  llmIntentArbiter: createTaskMutationArbiter(ctx),
3706
3711
  ...workflowForegroundSteeringLaunchOptions(foregroundControl, 0),
@@ -3708,6 +3713,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3708
3713
  unknownAgentDiagnosticContext: data.unknownAgentDiagnosticContext,
3709
3714
  runFanoutBudget: params.runFanoutAdmitted ? data.runFanoutBudget : { ...data.runFanoutBudget, parentPath: `${data.runFanoutBudget.parentPath ? `${data.runFanoutBudget.parentPath}/` : ""}single` },
3710
3715
  cwd: singleCwd,
3716
+ requestedCwd: data.requestedCwd,
3711
3717
  signal,
3712
3718
  interruptSignal: interruptController.signal,
3713
3719
  allowIntercomDetach: agentConfig.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
@@ -3751,6 +3757,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3751
3757
  },
3752
3758
  onDetachedExit: (result) => {
3753
3759
  try {
3760
+ if (worktreeSetup) {
3761
+ finalizeSingleWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: sourceCwd, agent: params.agent!, result });
3762
+ }
3754
3763
  try {
3755
3764
  updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: singleCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events, notify: true });
3756
3765
  } catch {
@@ -3998,6 +4007,7 @@ function workflowChildResult(
3998
4007
  result: AgentToolResult<Details>,
3999
4008
  childParams: Record<string, unknown> = {},
4000
4009
  resumeState?: SubagentState,
4010
+ forcedTerminalOutcome?: WorkflowTerminalOutcome,
4001
4011
  ): WorkflowScriptChildResult {
4002
4012
  const receiptOutput = result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n");
4003
4013
  const output = result.details.results.length === 1 && result.details.results[0]?.finalOutput !== undefined
@@ -4010,6 +4020,12 @@ function workflowChildResult(
4010
4020
  const detached = result.details.results.some((child) => child.detached);
4011
4021
  const interrupted = result.details.results.some((child) => child.interrupted);
4012
4022
  const stopped = result.details.results.some((child) => child.stopped);
4023
+ const terminalOutcome = forcedTerminalOutcome
4024
+ ?? (result.details.results.some((child) => child.timedOut)
4025
+ ? { state: "partial" as const, reason: "timeout" as const }
4026
+ : result.details.usageBudget?.exhausted || result.details.results.some((child) => child.turnBudgetExceeded || child.toolBudgetBlocked)
4027
+ ? { state: "partial" as const, reason: "budget_exhausted" as const }
4028
+ : undefined);
4013
4029
  const ok = result.isError !== true && !detached && !interrupted && !stopped;
4014
4030
  const artifactPaths = new Set<string>();
4015
4031
  if (result.details.asyncDir) artifactPaths.add(result.details.asyncDir);
@@ -4042,6 +4058,7 @@ function workflowChildResult(
4042
4058
  const continuationRunIds = [...new Set([resumeSourceRunId, runId].filter((value): value is string => Boolean(value)))];
4043
4059
  const outputReference = result.details.results.find((child) => child.savedOutputPath)?.savedOutputPath
4044
4060
  ?? result.details.results.find((child) => child.outputReference?.path)?.outputReference?.path;
4061
+ const outputPathMapping = typeof childParams.task === "string" ? outputPathMappingFromTask(childParams.task, outputReference) : undefined;
4045
4062
  const externalResult = result.details.results.length === 1 && result.details.results[0]?.runner?.type === "external-cli" ? result.details.results[0] : undefined;
4046
4063
  const externalStatus = result.details.asyncDir ? readStatus(result.details.asyncDir) : undefined;
4047
4064
  const externalStatusStep = externalStatus?.steps?.length === 1 && externalStatus.steps[0]?.runner?.type === "external-cli" ? externalStatus.steps[0] : undefined;
@@ -4052,6 +4069,7 @@ function workflowChildResult(
4052
4069
  return {
4053
4070
  key,
4054
4071
  ok,
4072
+ ...(terminalOutcome ? { terminalOutcome } : {}),
4055
4073
  ...(resolvedAgents.length === 1 ? { agent: resolvedAgents[0] } : {}),
4056
4074
  ...(runId ? { runId } : {}),
4057
4075
  output,
@@ -4063,6 +4081,7 @@ function workflowChildResult(
4063
4081
  ...(requestedContext ? { requestedContext } : {}),
4064
4082
  ...(resolvedContext ? { resolvedContext } : {}),
4065
4083
  ...(outputReference ? { outputReference } : {}),
4084
+ ...(outputPathMapping ? { outputPathMapping } : {}),
4066
4085
  ...(externalAdapter ? { externalAdapter } : {}),
4067
4086
  resumability,
4068
4087
  continuation: { runIds: continuationRunIds },
@@ -4071,6 +4090,13 @@ function workflowChildResult(
4071
4090
  };
4072
4091
  }
4073
4092
 
4093
+ function workflowOutputPathMappingSummary(children: WorkflowScriptChildResult[]): string {
4094
+ const mappings = children.flatMap((child) => child.outputPathMapping
4095
+ ? [`'${child.key}': requested ${child.outputPathMapping.requestedPath} -> saved ${child.outputPathMapping.savedPath}`]
4096
+ : []);
4097
+ return mappings.length > 0 ? ` Output path mappings: ${mappings.join("; ")}.` : "";
4098
+ }
4099
+
4074
4100
  function workflowSteerReceipt(key: string, result: AgentToolResult<Details>): WorkflowSteerResult {
4075
4101
  const steering = result.details.steering;
4076
4102
  const error = result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n") || undefined;
@@ -4110,8 +4136,14 @@ function terminalWorkflowReceipt(
4110
4136
  state: WorkflowReceiptState,
4111
4137
  children: WorkflowScriptChildResult[],
4112
4138
  workflowChildren?: WorkflowReceipt["workflowChildren"],
4139
+ terminalOutcome?: WorkflowTerminalOutcome,
4113
4140
  ): WorkflowReceipt {
4114
- return buildWorkflowReceipt({ workflowRunId, state, children, workflowChildren });
4141
+ return buildWorkflowReceipt({ workflowRunId, state, children, workflowChildren, terminalOutcome });
4142
+ }
4143
+
4144
+ function workflowFailureTerminalOutcome(error: unknown, _children: WorkflowScriptChildResult[], usageBudget: ReturnType<typeof usageBudgetState>): WorkflowTerminalOutcome | undefined {
4145
+ if (usageBudget?.exhausted) return { state: "partial", reason: "budget_exhausted" };
4146
+ return error instanceof WorkflowScriptError && error.errorKind === "timeout" ? { state: "partial", reason: "timeout" } : undefined;
4115
4147
  }
4116
4148
 
4117
4149
  function workflowFailureMessage(error: unknown, workflowRunId: string, children: WorkflowScriptChildResult[]): string {
@@ -4791,7 +4823,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4791
4823
  version: 1,
4792
4824
  childId: entry.key,
4793
4825
  status: "stopped",
4794
- reason: "user",
4826
+ reason: "subagent-action",
4795
4827
  source: "async",
4796
4828
  stepIndex: status.steps?.indexOf(projectedStep),
4797
4829
  agent: projectedStep.agent,
@@ -4836,7 +4868,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4836
4868
  launch: async (key, childParams, workflowSignal, admission) => {
4837
4869
  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);
4838
4870
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4839
- if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)), childParams, deps.state);
4871
+ if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)), childParams, deps.state, { state: "partial", reason: "budget_exhausted" });
4840
4872
  const childPhase = typeof childParams.phase === "string" && childParams.phase.trim() ? childParams.phase.trim() : undefined;
4841
4873
  const childLabel = typeof childParams.label === "string" && childParams.label.trim() ? childParams.label.trim() : undefined;
4842
4874
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, {
@@ -4923,7 +4955,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4923
4955
  });
4924
4956
  const returnPreview = formatWorkflowValue(workflow.value).slice(0, 1_000);
4925
4957
  const emitPreview = workflow.emits.length > 0 ? ` Emitted: ${workflow.emits.map(formatWorkflowValue).join(", ").slice(0, 1_000)}` : "";
4926
- const summary = `Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).`;
4958
+ const summary = `Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).${workflowOutputPathMappingSummary(workflow.children)}`;
4927
4959
  const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, summary, producedChildOutputPaths);
4928
4960
  const resultSummary = appendWorkflowOutputWarning(summary, outputWarning);
4929
4961
  const workflowUsage = sumResultsUsage(workflowResults);
@@ -4936,7 +4968,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4936
4968
  } catch (receiptError) {
4937
4969
  appendWorkflowEvent({ type: "subagent.workflow.receipt_write_failed", error: `Failed to persist async workflow receipt: ${receiptError instanceof Error ? receiptError.message : String(receiptError)}` });
4938
4970
  }
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;
4971
+ 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.outputPathMapping ? { outputPathMapping: child.outputPathMapping } : {}), ...(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;
4940
4972
  persist();
4941
4973
  persistClosed = true;
4942
4974
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}) });
@@ -4965,23 +4997,24 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4965
4997
  const promoted = promotePausedWorkflowIfSettled(status);
4966
4998
  if (promoted) status = promoted;
4967
4999
  }
4968
- const terminalSummary = status.state === "complete"
5000
+ const terminalSummary = `${status.state === "complete"
4969
5001
  ? "Workflow completed after detached child finished."
4970
- : status.error ?? (pauseForDetached ? "Workflow paused." : "Workflow failed.");
5002
+ : status.error ?? (pauseForDetached ? "Workflow paused." : "Workflow failed.")}${workflowOutputPathMappingSummary(partial.children)}`;
4971
5003
  const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, terminalSummary, producedChildOutputPaths);
4972
5004
  const resultSummary = appendWorkflowOutputWarning(terminalSummary, outputWarning);
4973
5005
  const receiptState: WorkflowReceiptState = status.state === "complete" ? "complete" : status.state === "paused" ? "paused" : status.state === "stopped" ? "stopped" : "failed";
4974
- const receipt = terminalWorkflowReceipt(workflowRunId, receiptState, partial.children, workflowChildren);
5006
+ const terminalOutcome = workflowFailureTerminalOutcome(error, partial.children, usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)));
5007
+ const receipt = terminalWorkflowReceipt(workflowRunId, receiptState, partial.children, workflowChildren, terminalOutcome);
4975
5008
  let workflowReceipt: { path: string; receipt: WorkflowReceipt } | undefined;
4976
5009
  try {
4977
5010
  workflowReceipt = { path: writeWorkflowReceipt(asyncDir, receipt), receipt };
4978
5011
  } catch (receiptError) {
4979
5012
  appendWorkflowEvent({ type: "subagent.workflow.receipt_write_failed", error: `Failed to persist async workflow receipt: ${receiptError instanceof Error ? receiptError.message : String(receiptError)}` });
4980
5013
  }
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;
5014
+ 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, ...(terminalOutcome ? { terminalOutcome } : {}), 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.terminalOutcome ? { terminalOutcome: child.terminalOutcome } : {}), ...(child.outputPathMapping ? { outputPathMapping: child.outputPathMapping } : {}), ...(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;
4982
5015
  persist();
4983
5016
  persistClosed = true;
4984
- appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}), ...(status.activityState ? { activityState: status.activityState } : {}) });
5017
+ appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(terminalOutcome ? { terminalOutcome } : {}), ...(status.error ? { error: status.error } : {}), ...(status.activityState ? { activityState: status.activityState } : {}) });
4985
5018
  } finally {
4986
5019
  persistClosed = true;
4987
5020
  deps.state.workflowControllers?.delete(workflowRunId);
@@ -5041,7 +5074,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5041
5074
  if (delegatedWorkflowPermit && childParams.async === true) throw new Error("Workflow child permit supports one foreground child only.");
5042
5075
  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);
5043
5076
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
5044
- if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)), childParams, deps.state);
5077
+ if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)), childParams, deps.state, { state: "partial", reason: "budget_exhausted" });
5045
5078
  const childPhase = typeof childParams.phase === "string" && childParams.phase.trim() ? childParams.phase.trim() : undefined;
5046
5079
  const childLabel = typeof childParams.label === "string" && childParams.label.trim() ? childParams.label.trim() : undefined;
5047
5080
  recordMissionWorkflowChild(missionBinding, _id, key, {
@@ -5111,6 +5144,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5111
5144
  if (workflow.emits.length > 0) sections.push(`Emitted:\n${workflow.emits.map(formatWorkflowValue).join("\n")}`);
5112
5145
  if (workflow.console.length > 0) sections.push(`Console:\n${workflow.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
5113
5146
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
5147
+ const outputMappings = workflowOutputPathMappingSummary(workflow.children).trim();
5148
+ if (outputMappings) sections.push(outputMappings);
5114
5149
  const workflowText = sections.join("\n\n");
5115
5150
  const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText, producedChildOutputPaths);
5116
5151
  const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
@@ -5126,11 +5161,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5126
5161
  if (partial.emits.length > 0) sections.push(`Emitted:\n${partial.emits.map(formatWorkflowValue).join("\n")}`);
5127
5162
  if (partial.console.length > 0) sections.push(`Console:\n${partial.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
5128
5163
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
5164
+ const outputMappings = workflowOutputPathMappingSummary(partial.children).trim();
5165
+ if (outputMappings) sections.push(outputMappings);
5129
5166
  const workflowText = sections.join("\n\n");
5130
5167
  const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText, producedChildOutputPaths);
5131
5168
  const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
5132
5169
  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);
5170
+ const terminalOutcome = workflowFailureTerminalOutcome(error, partial.children, usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)));
5171
+ const receipt = terminalWorkflowReceipt(_id, "failed", partial.children, workflowChildren, terminalOutcome);
5134
5172
  return attachWorkflowMission(withRunFanoutBudget({
5135
5173
  content: [{ type: "text", text: displayText }],
5136
5174
  isError: true,
@@ -5139,6 +5177,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5139
5177
  }
5140
5178
  }
5141
5179
  const directParams = requestParams;
5180
+ const requestedCwd = directParams.cwd;
5142
5181
  const requestCwd = resolveRequestedCwd(ctx.cwd, directParams.cwd);
5143
5182
  const paramsWithResolvedCwd = directParams.cwd === undefined ? directParams : { ...directParams, cwd: requestCwd };
5144
5183
  const action = paramsWithResolvedCwd.action;
@@ -5612,7 +5651,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5612
5651
  if (!isStoppableAsyncStatusStep(resolution.child.step)) return { content: [{ type: "text", text: `Child '${paramsWithResolvedCwd.childId}' in async run '${targetRunId}' is ${resolution.child.step.status}; stop only supports pending or running children.` }], isError: true, details: { mode: "management", results: [] } };
5613
5652
  const stopChild = deps.state.workflowChildStops?.get(targetRunId!);
5614
5653
  if (!stopChild) return { content: [{ type: "text", text: `Workflow ${targetRunId} child stop is unavailable in this extension runtime.` }], isError: true, details: { mode: "management", results: [] } };
5615
- if (!stopChild(resolution.child.id)) return { content: [{ type: "text", text: `Child '${paramsWithResolvedCwd.childId}' in workflow ${workflowRunId} is not available to stop.` }], isError: true, details: { mode: "management", results: [] } };
5654
+ if (!stopChild(resolution.child.id, `Workflow child '${resolution.child.id}' stopped.`)) return { content: [{ type: "text", text: `Child '${paramsWithResolvedCwd.childId}' in workflow ${workflowRunId} is not available to stop.` }], isError: true, details: { mode: "management", results: [] } };
5616
5655
  try {
5617
5656
  fs.appendFileSync(path.join(asyncJob.asyncDir, "events.jsonl"), `${JSON.stringify({
5618
5657
  type: "subagent.child-status",
@@ -5621,7 +5660,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5621
5660
  childId: resolution.child.id,
5622
5661
  status: "stopping",
5623
5662
  ts: Date.now(),
5624
- reason: "user",
5663
+ reason: "subagent-action",
5625
5664
  source: "async",
5626
5665
  stepIndex: resolution.child.index,
5627
5666
  agent: resolution.child.step.agent,
@@ -5635,17 +5674,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5635
5674
  }
5636
5675
  return { content: [{ type: "text", text: `Stop requested for child ${resolution.child.id} in async workflow ${workflowRunId}.` }], details: { mode: "management", results: [] } };
5637
5676
  }
5638
- workflowController.abort(new Error("Workflow stopped by user."));
5677
+ workflowController.abort(new Error("Workflow stopped."));
5639
5678
  return { content: [{ type: "text", text: `Stop requested for async workflow ${targetRunId}.` }], details: { mode: "management", results: [] } };
5640
5679
  }
5641
5680
  let resolved: ResolvedSubagentRunId | undefined;
5642
5681
  if (paramsWithResolvedCwd.dir) {
5643
5682
  try {
5644
5683
  const location = resolveAsyncRunLocation(paramsWithResolvedCwd, DIRS.async, DIRS.results);
5645
- const existingStatus = readStatus(location.asyncDir ?? "");
5646
- if (existingStatus?.mode === "workflow" && existingStatus.state === "running") {
5647
- return { content: [{ type: "text", text: `Workflow ${existingStatus.runId} is not controlled by this extension runtime; reload recovery cannot stop it safely.` }], isError: true, details: { mode: "management", results: [] } };
5648
- }
5649
5684
  const stopResult = stopAsyncRun(deps.state, location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir), deps.kill, location, paramsWithResolvedCwd.childId);
5650
5685
  return stopResult ?? { content: [{ type: "text", text: `No running or queued async run was found for '${targetRunId ?? paramsWithResolvedCwd.dir}'.` }], isError: true, details: { mode: "management", results: [] } };
5651
5686
  } catch (error) {
@@ -5662,12 +5697,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5662
5697
  }
5663
5698
  if (resolved?.kind === "nested") return { content: [{ type: "text", text: "action='stop' supports current-session top-level async runs only." }], isError: true, details: { mode: "management", results: [] } };
5664
5699
  if (resolved?.kind === "foreground") return { content: [{ type: "text", text: "action='stop' supports async runs only. Use action='interrupt' for foreground runs." }], isError: true, details: { mode: "management", results: [] } };
5665
- if (resolved?.kind === "async") {
5666
- const existingStatus = readStatus(resolved.location.asyncDir ?? "");
5667
- if (existingStatus?.mode === "workflow" && existingStatus.state === "running") {
5668
- return { content: [{ type: "text", text: `Workflow ${resolved.id} is not controlled by this extension runtime; reload recovery cannot stop it safely.` }], isError: true, details: { mode: "management", results: [] } };
5669
- }
5670
- }
5671
5700
  const stopResult = stopAsyncRun(
5672
5701
  deps.state,
5673
5702
  resolved?.kind === "async" ? resolved.id : targetRunId,
@@ -5869,6 +5898,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5869
5898
  parentModel,
5870
5899
  forkAvailableModels,
5871
5900
  agentConfig?.modelProvider ?? parentModel?.provider,
5901
+ { source: "inherited" },
5872
5902
  );
5873
5903
  const candidates = buildModelCandidates(
5874
5904
  primaryModel,
@@ -6085,6 +6115,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6085
6115
  const execData: ExecutionContextData = omitUndefinedProperties({
6086
6116
  params: effectiveParams,
6087
6117
  effectiveCwd,
6118
+ requestedCwd,
6088
6119
  ctx,
6089
6120
  signal,
6090
6121
  onUpdate: onUpdateWithContext,
@@ -9,11 +9,15 @@ import {
9
9
  type IntercomEventBus,
10
10
  type SingleResult,
11
11
  type SubagentState,
12
+ type WorkflowRecoveryAction,
13
+ type WorkflowTerminalOutcome,
14
+ type WorkflowTerminalResolution,
12
15
  } from "../../shared/types.ts";
13
16
  import { updateActiveRunIndex } from "../background/active-run-index.ts";
14
17
  import { resultFilePath, writeAsyncResultFile } from "../background/result-files.ts";
15
18
  import { resolveAsyncResumeTarget } from "../background/async-resume.ts";
16
19
  import { externalCliReceiptMetadata, normalizeExternalCliRunnerStatus } from "../shared/external-cli-contract.ts";
20
+ import { outputPathMappingFromTask } from "../shared/single-output.ts";
17
21
  import { readWorkflowReceipt, workflowReceiptPath, writeWorkflowReceipt, type WorkflowReceipt } from "../../workflows/workflow-receipt.ts";
18
22
  import { workflowChildSummary } from "../../workflows/workflow-child-summary.ts";
19
23
 
@@ -31,18 +35,33 @@ function childSucceeded(result: Pick<SingleResult, "exitCode" | "error" | "inter
31
35
 
32
36
  const UNSUPPORTED_DETACHED_WORKFLOW_CONTINUATION = "unsupported-continuation: detached workflow child settled, but JavaScript workflow continuation was not persisted. Resume the workflow explicitly instead of treating the completed child as top-level workflow completion.";
33
37
  const INTERRUPTED_DETACHED_CHILD = "Interrupted. Waiting for explicit next action.";
38
+ type WorkflowStatusStep = NonNullable<AsyncStatus["steps"]>[number] & {
39
+ outputPathMapping?: { requestedPath: string; savedPath: string };
40
+ interrupted?: boolean;
41
+ };
42
+
43
+ function findWorkflowStep(status: AsyncStatus, childRunId: string, workflowKey?: string): WorkflowStatusStep | undefined {
44
+ return status.steps?.find((candidate) => candidate.runId === childRunId)
45
+ ?? (workflowKey ? status.steps?.find((candidate) => candidate.workflowKey === workflowKey) : undefined) as WorkflowStatusStep | undefined;
46
+ }
34
47
 
35
48
  export function applyDetachedChildToPausedWorkflow(
36
49
  status: AsyncStatus,
37
- input: { childRunId: string; result: Pick<SingleResult, "exitCode" | "error" | "interrupted" | "sessionFile">; workflowKey?: string },
50
+ input: { childRunId: string; result: Pick<SingleResult, "exitCode" | "error" | "interrupted" | "sessionFile" | "stopped">; workflowKey?: string },
38
51
  ): AsyncStatus | undefined {
39
52
  if (status.mode !== "workflow" || status.state !== "paused") return undefined;
40
53
  const next = cloneWorkflowStatus(status);
41
- const step = next.steps?.find((candidate) => candidate.runId === input.childRunId)
42
- ?? (input.workflowKey ? next.steps?.find((candidate) => candidate.workflowKey === input.workflowKey) : undefined);
54
+ const step = findWorkflowStep(next, input.childRunId, input.workflowKey);
43
55
  if (!step) return undefined;
44
56
  const succeeded = childSucceeded(input.result);
45
- const failedSiblingError = next.steps?.find((candidate) => candidate !== step && candidate.status === "failed" && candidate.error)?.error;
57
+ const failedSiblingError = next.steps?.find((candidate) => {
58
+ const candidateStep = candidate as WorkflowStatusStep;
59
+ return candidateStep !== step && candidateStep.status === "failed" && !candidateStep.interrupted && candidateStep.error;
60
+ })?.error;
61
+ const interruptedChildError = next.steps?.find((candidate) => {
62
+ const candidateStep = candidate as WorkflowStatusStep;
63
+ return candidateStep.status === "failed" && candidateStep.interrupted && candidateStep.error;
64
+ })?.error;
46
65
  const updatedAt = Date.now();
47
66
  step.status = succeeded ? "completed" : "failed";
48
67
  step.endedAt = updatedAt;
@@ -50,13 +69,24 @@ export function applyDetachedChildToPausedWorkflow(
50
69
  delete step.currentTool;
51
70
  delete step.currentToolStartedAt;
52
71
  if (input.result.sessionFile) step.sessionFile = input.result.sessionFile;
53
- if (succeeded) delete step.error;
54
- else if (input.result.interrupted) step.error = input.result.error ?? INTERRUPTED_DETACHED_CHILD;
55
- else if (input.result.error) step.error = input.result.error;
72
+ if (succeeded) {
73
+ delete step.error;
74
+ delete step.interrupted;
75
+ } else if (input.result.interrupted || input.result.stopped) {
76
+ step.error = input.result.error ?? INTERRUPTED_DETACHED_CHILD;
77
+ step.interrupted = true;
78
+ if (input.result.stopped) step.stopped = true;
79
+ } else {
80
+ delete step.interrupted;
81
+ delete step.stopped;
82
+ if (input.result.error) step.error = input.result.error;
83
+ }
56
84
  next.lastUpdate = updatedAt;
57
85
  const promoted = promotePausedWorkflowIfSettled(next);
58
- if (promoted?.state === "failed" && input.result.interrupted && !failedSiblingError) promoted.error = input.result.error ?? INTERRUPTED_DETACHED_CHILD;
86
+ if (promoted?.state === "failed" && failedSiblingError) promoted.error = failedSiblingError;
87
+ else if (promoted?.state === "failed" && (input.result.interrupted || input.result.stopped)) promoted.error = input.result.error ?? INTERRUPTED_DETACHED_CHILD;
59
88
  else if (promoted?.state === "failed" && input.result.error) promoted.error = input.result.error;
89
+ else if (promoted?.state === "failed" && interruptedChildError) promoted.error = interruptedChildError;
60
90
  const resolved = promoted ?? next;
61
91
  const workflowState = resolved.state === "complete" ? "completed" : resolved.state === "paused" ? "paused" : resolved.state === "stopped" ? "stopped" : "failed";
62
92
  resolved.workflowChildren = workflowChildSummary({ parentToolCallId: resolved.toolCallId ?? resolved.runId, workflowRunId: resolved.runId, workflowState, inventoryComplete: true, trace: resolved.workflow?.trace, steps: resolved.steps });
@@ -82,8 +112,37 @@ export function promotePausedWorkflowIfSettled(status: AsyncStatus): AsyncStatus
82
112
  return next;
83
113
  }
84
114
 
85
- function workflowResultChildren(status: AsyncStatus, childRunId: string, result: SingleResult, existingResults: unknown): unknown {
115
+ function workflowResolution(status: AsyncStatus, result: Pick<SingleResult, "interrupted">): WorkflowTerminalResolution | undefined {
116
+ if (status.state !== "complete" && status.state !== "failed") return undefined;
117
+ if (status.steps?.some((step) => {
118
+ const candidate = step as WorkflowStatusStep;
119
+ return candidate.status === "failed" && !candidate.interrupted;
120
+ })) return "failed-child";
121
+ if (result.interrupted || status.steps?.some((step) => {
122
+ const candidate = step as WorkflowStatusStep;
123
+ return candidate.status === "stopped" || candidate.stopped || candidate.interrupted || candidate.error === INTERRUPTED_DETACHED_CHILD;
124
+ })) return "interrupted-child";
125
+ return "settled-awaiting-resume";
126
+ }
127
+
128
+ function workflowRecovery(receipt: WorkflowReceipt | undefined): WorkflowRecoveryAction[] {
129
+ if (!receipt) return [];
130
+ return Object.values(receipt.entries).flatMap((entry) => entry.resumability.state === "resumable"
131
+ ? [{ key: entry.key, call: "runs.run" as const, resume: { workflowRunId: receipt.workflowRunId, key: entry.key, latest: true as const }, taskRequired: true as const }]
132
+ : []);
133
+ }
134
+
135
+ function resultTerminalOutcome(result: Pick<SingleResult, "timedOut" | "turnBudgetExceeded" | "toolBudgetBlocked">): WorkflowTerminalOutcome | undefined {
136
+ if (result.timedOut) return { state: "partial", reason: "timeout" };
137
+ if (result.turnBudgetExceeded || result.toolBudgetBlocked) return { state: "partial", reason: "budget_exhausted" };
138
+ return undefined;
139
+ }
140
+
141
+ function workflowResultChildren(status: AsyncStatus, childRunId: string, result: SingleResult, existingResults: unknown, receipt?: WorkflowReceipt): unknown {
86
142
  const output = getSingleResultOutput(result);
143
+ const outputReference = result.savedOutputPath ?? result.outputReference?.path;
144
+ const outputPathMapping = outputPathMappingFromTask(result.task, outputReference);
145
+ const terminalOutcome = resultTerminalOutcome(result);
87
146
  if (Array.isArray(existingResults)) {
88
147
  return existingResults.map((entry) => {
89
148
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) return entry;
@@ -95,28 +154,56 @@ function workflowResultChildren(status: AsyncStatus, childRunId: string, result:
95
154
  output,
96
155
  outputState: output.trim() ? "present" : "absent",
97
156
  detached: undefined,
98
- ...(result.interrupted ? { interrupted: true } : {}),
157
+ ...(outputReference ? { outputReference } : {}),
158
+ ...(outputPathMapping ? { outputPathMapping } : {}),
159
+ ...(result.interrupted || result.stopped ? { interrupted: true } : {}),
160
+ ...(result.stopped ? { stopped: true } : {}),
161
+ ...(terminalOutcome ? { terminalOutcome } : {}),
99
162
  ...(result.error ? { error: result.error } : {}),
100
163
  };
101
164
  });
102
165
  }
103
- return status.steps?.map((step) => ({
166
+ return status.steps?.map((step: WorkflowStatusStep) => ({
104
167
  workflowKey: step.workflowKey,
105
168
  agent: step.agent,
106
169
  runId: step.runId,
107
170
  success: step.status === "completed" || step.status === "complete",
108
171
  output: step.runId === childRunId ? output : "",
109
172
  outputState: step.runId === childRunId && output.trim() ? "present" : "absent",
110
- ...(step.runId === childRunId && result.interrupted ? { interrupted: true } : {}),
173
+ ...(step.runId === childRunId && outputReference ? { outputReference } : step.workflowKey && receipt?.entries[step.workflowKey]?.outputReference ? { outputReference: receipt.entries[step.workflowKey]!.outputReference } : {}),
174
+ ...(step.runId === childRunId && outputPathMapping ? { outputPathMapping } : step.outputPathMapping ? { outputPathMapping: step.outputPathMapping } : {}),
175
+ ...(step.interrupted ? { interrupted: true } : {}),
176
+ ...(step.runId === childRunId && terminalOutcome ? { terminalOutcome } : step.workflowKey && receipt?.entries[step.workflowKey]?.terminalOutcome ? { terminalOutcome: receipt.entries[step.workflowKey]!.terminalOutcome } : {}),
177
+ ...(step.stopped ? { stopped: true } : {}),
111
178
  ...(step.error ? { error: step.error } : {}),
112
179
  }));
113
180
  }
114
181
 
182
+ function workflowResultOutputPathMappingSummary(results: unknown): string {
183
+ if (!Array.isArray(results)) return "";
184
+ const mappings = results.flatMap((entry) => {
185
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
186
+ const child = entry as Record<string, unknown>;
187
+ const mapping = child.outputPathMapping;
188
+ if (!mapping || typeof mapping !== "object" || Array.isArray(mapping)) return [];
189
+ const { requestedPath, savedPath } = mapping as Record<string, unknown>;
190
+ if (typeof requestedPath !== "string" || typeof savedPath !== "string") return [];
191
+ const key = typeof child.workflowKey === "string" ? child.workflowKey : "child";
192
+ return [`'${key}': requested ${requestedPath} -> saved ${savedPath}`];
193
+ });
194
+ return mappings.length > 0 ? ` Output path mappings: ${mappings.join("; ")}.` : "";
195
+ }
196
+
115
197
  function publishedWorkflowResult(status: AsyncStatus, childRunId: string, result: SingleResult, asyncDir: string, existing?: Record<string, unknown>, receipt?: WorkflowReceipt): Record<string, unknown> {
116
198
  const sessionId = status.sessionId ?? (typeof existing?.sessionId === "string" ? existing.sessionId : undefined);
117
- const summary = status.state === "complete"
199
+ const resolution = workflowResolution(status, result);
200
+ const recovery = workflowRecovery(receipt);
201
+ const results = workflowResultChildren(status, childRunId, result, existing?.results, receipt);
202
+ const summary = `${resolution === "settled-awaiting-resume"
203
+ ? `Workflow lanes settled after detached child ${childRunId} finished. JavaScript workflow continuation was not persisted.${recovery.length ? " Use the listed keyed recovery action to continue a child." : " No retained child is resumable."}`
204
+ : status.state === "complete"
118
205
  ? `Workflow completed after detached child ${childRunId} finished.`
119
- : status.error ?? (typeof existing?.summary === "string" ? existing.summary : undefined);
206
+ : status.error ?? (typeof existing?.summary === "string" ? existing.summary : "Workflow failed.")}${workflowResultOutputPathMappingSummary(results)}`;
120
207
  return {
121
208
  ...(existing ?? {}),
122
209
  id: status.runId,
@@ -131,9 +218,11 @@ function publishedWorkflowResult(status: AsyncStatus, childRunId: string, result
131
218
  activityState: status.activityState,
132
219
  endedAt: status.endedAt,
133
220
  timestamp: Date.now(),
134
- results: workflowResultChildren(status, childRunId, result, existing?.results),
221
+ results,
135
222
  workflow: status.workflow,
136
223
  workflowChildren: status.workflowChildren,
224
+ ...(receipt?.terminalOutcome ? { terminalOutcome: receipt.terminalOutcome } : {}),
225
+ ...(resolution ? { workflowResolution: resolution, recovery } : {}),
137
226
  reconciledFromDetachedChild: childRunId,
138
227
  ...(receipt ? { workflowReceipt: { path: path.join(asyncDir, "workflow-receipt.json"), receipt } } : {}),
139
228
  asyncDir,
@@ -143,7 +232,7 @@ function publishedWorkflowResult(status: AsyncStatus, childRunId: string, result
143
232
  };
144
233
  }
145
234
 
146
- function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, result: SingleResult, asyncDir: string): WorkflowReceipt | undefined {
235
+ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, result: SingleResult, asyncDir: string, resolution: WorkflowTerminalResolution | undefined): WorkflowReceipt | undefined {
147
236
  const receiptPath = workflowReceiptPath(DIRS.async, status.runId);
148
237
  if (!fs.existsSync(receiptPath)) return undefined;
149
238
  const receipt = readWorkflowReceipt(DIRS.async, status.runId);
@@ -165,6 +254,7 @@ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, resul
165
254
  const externalRunner = normalizeExternalCliRunnerStatus(result.runner?.type === "external-cli" ? result.runner : externalStep?.runner);
166
255
  const externalProcess = result.externalProcess ?? externalStep?.externalProcess;
167
256
  const externalAdapter = externalRunner ? externalCliReceiptMetadata({ runner: externalRunner, externalProcess, outputReference }) : entry.externalAdapter;
257
+ const childTerminalOutcome = resultTerminalOutcome(result) ?? entry.terminalOutcome;
168
258
  if (externalAdapter) resumability = { state: "not-resumable", reason: externalAdapter.nonResumableReason };
169
259
  const updatedEntry: WorkflowReceipt["entries"][string] = resumability.state === "resumable"
170
260
  ? {
@@ -174,6 +264,7 @@ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, resul
174
264
  latestRunId: entry.latestRunId ?? childRunId,
175
265
  resumability,
176
266
  ...(outputReference ? { outputReference } : {}),
267
+ ...(childTerminalOutcome ? { terminalOutcome: childTerminalOutcome } : {}),
177
268
  ...(externalAdapter ? { externalAdapter } : {}),
178
269
  }
179
270
  : {
@@ -182,6 +273,7 @@ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, resul
182
273
  ...(step.context ? { resolvedContext: step.context } : {}),
183
274
  resumability,
184
275
  ...(outputReference ? { outputReference } : {}),
276
+ ...(childTerminalOutcome ? { terminalOutcome: childTerminalOutcome } : {}),
185
277
  ...(externalAdapter ? { externalAdapter } : {}),
186
278
  };
187
279
  const next: WorkflowReceipt = {
@@ -192,7 +284,14 @@ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, resul
192
284
  [key]: updatedEntry,
193
285
  },
194
286
  workflowChildren: status.workflowChildren,
287
+ ...(receipt.terminalOutcome ? { terminalOutcome: receipt.terminalOutcome } : {}),
288
+ ...(resolution ? { workflowResolution: resolution } : {}),
195
289
  };
290
+ if (resolution) next.recovery = workflowRecovery(next);
291
+ else {
292
+ delete next.workflowResolution;
293
+ delete next.recovery;
294
+ }
196
295
  writeWorkflowReceipt(asyncDir, next);
197
296
  return next;
198
297
  }
@@ -224,6 +323,10 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
224
323
  workflowKey: input.workflowKey,
225
324
  });
226
325
  if (!next) return false;
326
+ const outputReference = input.result.savedOutputPath ?? input.result.outputReference?.path;
327
+ const outputPathMapping = outputPathMappingFromTask(input.result.task, outputReference);
328
+ const settledStep = findWorkflowStep(next, input.childRunId, input.workflowKey);
329
+ if (settledStep && outputPathMapping) settledStep.outputPathMapping = outputPathMapping;
227
330
  writeAtomicJson(path.join(asyncDir, "status.json"), next);
228
331
  updateActiveRunIndex(asyncDir, next.state, next.toolCallId);
229
332
  if (job) {
@@ -243,8 +346,9 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
243
346
  }
244
347
  let receipt: WorkflowReceipt | undefined;
245
348
  let receiptError: string | undefined;
349
+ const resolution = workflowResolution(next, input.result);
246
350
  try {
247
- receipt = reconcileWorkflowReceipt(next, input.childRunId, input.result, asyncDir);
351
+ receipt = reconcileWorkflowReceipt(next, input.childRunId, input.result, asyncDir, resolution);
248
352
  } catch (error) {
249
353
  receiptError = `Failed to reconcile async workflow receipt: ${error instanceof Error ? error.message : String(error)}`;
250
354
  }
@@ -260,11 +364,15 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
260
364
  });
261
365
  }
262
366
  if (next.state === "complete" || next.state === "failed") {
367
+ if (!resolution) throw new Error(`Terminal detached workflow '${input.workflowRunId}' has no resolution classification.`);
263
368
  appendDetachedWorkflowEvent(asyncDir, {
264
369
  ts: Date.now(),
265
370
  runId: input.workflowRunId,
266
371
  type: "subagent.workflow.completed",
267
372
  state: next.state,
373
+ workflowResolution: resolution,
374
+ ...(receipt?.terminalOutcome ? { terminalOutcome: receipt.terminalOutcome } : {}),
375
+ recovery: workflowRecovery(receipt),
268
376
  ...(next.error ? { error: next.error } : {}),
269
377
  reconciledFromDetachedChild: input.childRunId,
270
378
  });
@@ -276,6 +384,9 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
276
384
  agent: "workflow",
277
385
  success: next.state === "complete",
278
386
  state: next.state,
387
+ workflowResolution: resolution,
388
+ ...(receipt?.terminalOutcome ? { terminalOutcome: receipt.terminalOutcome } : {}),
389
+ recovery: workflowRecovery(receipt),
279
390
  summary: typeof published.summary === "string" ? published.summary : (next.state === "complete" ? "Workflow completed." : next.error),
280
391
  reconciledFromDetachedChild: input.childRunId,
281
392
  ...(Array.isArray(published.results) ? { results: published.results } : {}),
@@ -47,6 +47,7 @@ interface CompletionMutationGuardInput {
47
47
  messages: Message[];
48
48
  tools?: string[];
49
49
  mcpDirectTools?: string[];
50
+ mutationTools?: string[];
50
51
  toolAvailabilityError?: string;
51
52
  mutationEvidence?: TrackedMutationEvidence;
52
53
  }
@@ -128,7 +129,7 @@ function hasCheckpointMutationEvidence(message: Message): boolean {
128
129
  && data.beforeCommit !== data.afterCommit;
129
130
  }
130
131
 
131
- export function hasMutationToolCall(messages: Message[]): boolean {
132
+ export function hasMutationToolCall(messages: Message[], mutationTools?: readonly string[]): boolean {
132
133
  for (const message of messages) {
133
134
  if (hasCheckpointMutationEvidence(message)) return true;
134
135
  if (message.role !== "assistant") continue;
@@ -138,7 +139,7 @@ export function hasMutationToolCall(messages: Message[]): boolean {
138
139
  const args = typeof part.arguments === "object" && part.arguments !== null && !Array.isArray(part.arguments)
139
140
  ? part.arguments as Record<string, unknown>
140
141
  : {};
141
- if (isMutatingTool(part.name, args)) return true;
142
+ if (isMutatingTool(part.name, args, mutationTools)) return true;
142
143
  }
143
144
  }
144
145
  return false;
@@ -239,7 +240,7 @@ export function evaluateCompletionMutationGuard(input: CompletionMutationGuardIn
239
240
  const expectedMutation = hasMutationToolCapability(input.tools, input.mcpDirectTools)
240
241
  ? expectsImplementationMutation(input.agent, input.task)
241
242
  : false;
242
- const attemptedMutation = hasMutationToolCall(input.messages) || input.mutationEvidence?.attemptedMutation === true;
243
+ const attemptedMutation = hasMutationToolCall(input.messages, input.mutationTools) || input.mutationEvidence?.attemptedMutation === true;
243
244
  const noEditChallengeComplete = isImplementationChallengeTask(input.task)
244
245
  && reportsNoBetterChallengeChange(input.messages);
245
246
  return {