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.
- package/CHANGELOG.md +39 -0
- package/docs/agents.md +13 -6
- package/docs/extension-api.md +39 -0
- package/docs/models.md +3 -3
- package/docs/tool-reference.md +3 -2
- package/docs/workflows.md +24 -0
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +2 -0
- package/skills/pi-subagents/references/execution-controls.md +1 -1
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/agent-management.ts +23 -0
- package/src/agents/agent-serializer.ts +5 -0
- package/src/agents/agents.ts +38 -3
- package/src/agents/runtime-agent-events.ts +70 -0
- package/src/agents/runtime-agent-registry.ts +10 -2
- package/src/api/agents.ts +10 -5
- package/src/api/preflight.ts +26 -1
- package/src/extension/index.ts +15 -4
- package/src/extension/rpc.ts +35 -14
- package/src/extension/schemas.ts +2 -2
- package/src/extension/tool-description.ts +8 -4
- package/src/integrations/herdr-status.ts +5 -0
- package/src/runs/background/async-execution.ts +29 -12
- package/src/runs/background/async-resume.ts +6 -2
- package/src/runs/background/async-status.ts +2 -0
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/notify.ts +26 -3
- package/src/runs/background/result-delivery-ownership.ts +45 -0
- package/src/runs/background/result-watcher.ts +27 -10
- package/src/runs/background/subagent-runner.ts +71 -21
- package/src/runs/foreground/execution.ts +20 -1
- package/src/runs/foreground/subagent-executor.ts +57 -26
- package/src/runs/foreground/workflow-detach-reconcile.ts +128 -17
- package/src/runs/shared/completion-guard.ts +4 -3
- package/src/runs/shared/dynamic-fanout.ts +3 -3
- package/src/runs/shared/fast-mode-extension.ts +5 -5
- package/src/runs/shared/launch-cwd.ts +16 -0
- package/src/runs/shared/long-running-guard.ts +2 -1
- package/src/runs/shared/mcp-config-sources.ts +386 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +155 -42
- package/src/runs/shared/model-exclusions.ts +12 -0
- package/src/runs/shared/model-fallback.ts +16 -4
- package/src/runs/shared/parallel-utils.ts +8 -1
- package/src/runs/shared/pi-args.ts +61 -6
- package/src/runs/shared/single-output.ts +17 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
- package/src/shared/formatters.ts +6 -0
- package/src/shared/launch-contract.ts +4 -0
- package/src/shared/types.ts +42 -1
- package/src/shared/utils.ts +6 -29
- package/src/slash/subagents-admin.ts +3 -0
- package/src/tui/render.ts +14 -6
- package/src/workflows/scripted-workflow.ts +52 -5
- 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: "
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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) =>
|
|
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)
|
|
54
|
-
|
|
55
|
-
|
|
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" &&
|
|
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
|
|
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
|
-
...(
|
|
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 &&
|
|
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
|
|
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 :
|
|
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
|
|
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 {
|