pi-subagents 0.56.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 +92 -0
- package/agents/claude-code-writer.md +15 -0
- package/agents/claude-code.md +15 -0
- package/agents/codex-exec-writer.md +15 -0
- package/agents/codex-exec.md +15 -0
- package/agents/cursor-agent-writer.md +14 -0
- package/agents/cursor-agent.md +14 -0
- package/docs/agents.md +124 -21
- package/docs/configuration.md +37 -0
- package/docs/extension-api.md +41 -2
- package/docs/models.md +3 -3
- package/docs/observability.md +8 -7
- package/docs/tool-reference.md +14 -3
- package/docs/workflows.md +44 -0
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +2 -0
- package/skills/pi-subagents/references/execution-controls.md +2 -2
- 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 +36 -5
- package/src/agents/agent-refinements.ts +4 -4
- package/src/agents/agent-serializer.ts +5 -0
- package/src/agents/agents.ts +257 -51
- package/src/agents/builtin-names.ts +6 -0
- package/src/agents/runtime-agent-events.ts +70 -0
- package/src/agents/runtime-agent-registry.ts +18 -4
- package/src/api/agents.ts +10 -5
- package/src/api/preflight.ts +28 -3
- package/src/extension/config.ts +70 -0
- package/src/extension/doctor.ts +3 -3
- package/src/extension/index.ts +33 -8
- package/src/extension/public-execution.ts +29 -13
- package/src/extension/rpc.ts +55 -19
- package/src/extension/schemas.ts +6 -5
- package/src/extension/tool-description.ts +18 -12
- package/src/inspectors/herdr/actions.ts +2 -1
- package/src/inspectors/herdr/inspector-runner.ts +2 -10
- package/src/inspectors/herdr/session-roots-codec.ts +42 -0
- package/src/integrations/herdr-status.ts +51 -3
- package/src/runs/background/active-async-capacity.ts +77 -10
- package/src/runs/background/async-execution.ts +127 -19
- package/src/runs/background/async-job-tracker.ts +5 -0
- package/src/runs/background/async-resume.ts +6 -2
- package/src/runs/background/async-retention.ts +20 -3
- package/src/runs/background/async-status.ts +7 -0
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/chain-root-attachment.ts +15 -1
- package/src/runs/background/fleet-view.ts +16 -10
- package/src/runs/background/inspect-rpc.ts +8 -8
- package/src/runs/background/notify.ts +26 -3
- package/src/runs/background/result-delivery-ownership.ts +45 -0
- package/src/runs/background/result-files.ts +27 -14
- package/src/runs/background/result-watcher.ts +36 -15
- package/src/runs/background/run-status.ts +33 -6
- package/src/runs/background/scheduled-runs.ts +7 -1
- package/src/runs/background/subagent-runner.ts +239 -56
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/execution.ts +92 -11
- package/src/runs/foreground/foreground-control.ts +6 -0
- package/src/runs/foreground/foreground-history.ts +22 -1
- package/src/runs/foreground/subagent-executor.ts +322 -102
- package/src/runs/foreground/workflow-detach-reconcile.ts +144 -18
- package/src/runs/shared/child-protocol.ts +21 -7
- package/src/runs/shared/claude-code-adapter.ts +129 -0
- package/src/runs/shared/codex-exec-adapter.ts +129 -0
- package/src/runs/shared/completion-guard.ts +4 -3
- package/src/runs/shared/cursor-agent-adapter.ts +114 -0
- package/src/runs/shared/dynamic-fanout.ts +3 -3
- package/src/runs/shared/external-cli-contract.ts +167 -0
- package/src/runs/shared/external-cli-preflight.ts +122 -0
- package/src/runs/shared/external-cli-runner.ts +348 -55
- 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 +69 -7
- package/src/runs/shared/model-fallback.ts +39 -5
- package/src/runs/shared/mutation-evidence.ts +7 -2
- package/src/runs/shared/nested-events.ts +3 -1
- package/src/runs/shared/nested-render.ts +2 -2
- package/src/runs/shared/parallel-utils.ts +8 -1
- package/src/runs/shared/pi-args.ts +61 -6
- package/src/runs/shared/process-signal.ts +13 -0
- package/src/runs/shared/run-history.ts +21 -1
- package/src/runs/shared/single-output.ts +17 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
- package/src/shared/fork-context.ts +21 -0
- package/src/shared/formatters.ts +13 -1
- package/src/shared/launch-contract.ts +4 -0
- package/src/shared/pruned-fork.ts +450 -0
- package/src/shared/session-file-trust.ts +19 -0
- package/src/shared/session-tokens.ts +14 -3
- package/src/shared/settings.ts +10 -2
- package/src/shared/shortcuts.ts +17 -0
- package/src/shared/types.ts +160 -10
- package/src/shared/utils.ts +6 -29
- package/src/shared/workflow-child-permit.ts +116 -0
- package/src/slash/delegation-adapters.ts +0 -1
- package/src/slash/slash-commands.ts +8 -6
- package/src/slash/subagents-admin.ts +3 -0
- package/src/tui/fleet-status.ts +27 -10
- package/src/tui/fleet-transcript.ts +11 -5
- package/src/tui/fleet.ts +28 -13
- package/src/tui/render.ts +55 -21
- package/src/workflows/scripted-workflow.ts +299 -31
- package/src/workflows/workflow-child-summary.ts +117 -0
- package/src/workflows/workflow-receipt.ts +155 -5
|
@@ -9,11 +9,17 @@ 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";
|
|
19
|
+
import { externalCliReceiptMetadata, normalizeExternalCliRunnerStatus } from "../shared/external-cli-contract.ts";
|
|
20
|
+
import { outputPathMappingFromTask } from "../shared/single-output.ts";
|
|
16
21
|
import { readWorkflowReceipt, workflowReceiptPath, writeWorkflowReceipt, type WorkflowReceipt } from "../../workflows/workflow-receipt.ts";
|
|
22
|
+
import { workflowChildSummary } from "../../workflows/workflow-child-summary.ts";
|
|
17
23
|
|
|
18
24
|
function cloneWorkflowStatus(status: AsyncStatus): AsyncStatus {
|
|
19
25
|
return {
|
|
@@ -29,18 +35,33 @@ function childSucceeded(result: Pick<SingleResult, "exitCode" | "error" | "inter
|
|
|
29
35
|
|
|
30
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.";
|
|
31
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
|
+
}
|
|
32
47
|
|
|
33
48
|
export function applyDetachedChildToPausedWorkflow(
|
|
34
49
|
status: AsyncStatus,
|
|
35
|
-
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 },
|
|
36
51
|
): AsyncStatus | undefined {
|
|
37
52
|
if (status.mode !== "workflow" || status.state !== "paused") return undefined;
|
|
38
53
|
const next = cloneWorkflowStatus(status);
|
|
39
|
-
const step = next
|
|
40
|
-
?? (input.workflowKey ? next.steps?.find((candidate) => candidate.workflowKey === input.workflowKey) : undefined);
|
|
54
|
+
const step = findWorkflowStep(next, input.childRunId, input.workflowKey);
|
|
41
55
|
if (!step) return undefined;
|
|
42
56
|
const succeeded = childSucceeded(input.result);
|
|
43
|
-
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;
|
|
44
65
|
const updatedAt = Date.now();
|
|
45
66
|
step.status = succeeded ? "completed" : "failed";
|
|
46
67
|
step.endedAt = updatedAt;
|
|
@@ -48,14 +69,28 @@ export function applyDetachedChildToPausedWorkflow(
|
|
|
48
69
|
delete step.currentTool;
|
|
49
70
|
delete step.currentToolStartedAt;
|
|
50
71
|
if (input.result.sessionFile) step.sessionFile = input.result.sessionFile;
|
|
51
|
-
if (succeeded)
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
}
|
|
54
84
|
next.lastUpdate = updatedAt;
|
|
55
85
|
const promoted = promotePausedWorkflowIfSettled(next);
|
|
56
|
-
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;
|
|
57
88
|
else if (promoted?.state === "failed" && input.result.error) promoted.error = input.result.error;
|
|
58
|
-
|
|
89
|
+
else if (promoted?.state === "failed" && interruptedChildError) promoted.error = interruptedChildError;
|
|
90
|
+
const resolved = promoted ?? next;
|
|
91
|
+
const workflowState = resolved.state === "complete" ? "completed" : resolved.state === "paused" ? "paused" : resolved.state === "stopped" ? "stopped" : "failed";
|
|
92
|
+
resolved.workflowChildren = workflowChildSummary({ parentToolCallId: resolved.toolCallId ?? resolved.runId, workflowRunId: resolved.runId, workflowState, inventoryComplete: true, trace: resolved.workflow?.trace, steps: resolved.steps });
|
|
93
|
+
return resolved;
|
|
59
94
|
}
|
|
60
95
|
|
|
61
96
|
export function promotePausedWorkflowIfSettled(status: AsyncStatus): AsyncStatus | undefined {
|
|
@@ -77,8 +112,37 @@ export function promotePausedWorkflowIfSettled(status: AsyncStatus): AsyncStatus
|
|
|
77
112
|
return next;
|
|
78
113
|
}
|
|
79
114
|
|
|
80
|
-
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 {
|
|
81
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);
|
|
82
146
|
if (Array.isArray(existingResults)) {
|
|
83
147
|
return existingResults.map((entry) => {
|
|
84
148
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return entry;
|
|
@@ -90,28 +154,56 @@ function workflowResultChildren(status: AsyncStatus, childRunId: string, result:
|
|
|
90
154
|
output,
|
|
91
155
|
outputState: output.trim() ? "present" : "absent",
|
|
92
156
|
detached: undefined,
|
|
93
|
-
...(
|
|
157
|
+
...(outputReference ? { outputReference } : {}),
|
|
158
|
+
...(outputPathMapping ? { outputPathMapping } : {}),
|
|
159
|
+
...(result.interrupted || result.stopped ? { interrupted: true } : {}),
|
|
160
|
+
...(result.stopped ? { stopped: true } : {}),
|
|
161
|
+
...(terminalOutcome ? { terminalOutcome } : {}),
|
|
94
162
|
...(result.error ? { error: result.error } : {}),
|
|
95
163
|
};
|
|
96
164
|
});
|
|
97
165
|
}
|
|
98
|
-
return status.steps?.map((step) => ({
|
|
166
|
+
return status.steps?.map((step: WorkflowStatusStep) => ({
|
|
99
167
|
workflowKey: step.workflowKey,
|
|
100
168
|
agent: step.agent,
|
|
101
169
|
runId: step.runId,
|
|
102
170
|
success: step.status === "completed" || step.status === "complete",
|
|
103
171
|
output: step.runId === childRunId ? output : "",
|
|
104
172
|
outputState: step.runId === childRunId && output.trim() ? "present" : "absent",
|
|
105
|
-
...(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 } : {}),
|
|
106
178
|
...(step.error ? { error: step.error } : {}),
|
|
107
179
|
}));
|
|
108
180
|
}
|
|
109
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
|
+
|
|
110
197
|
function publishedWorkflowResult(status: AsyncStatus, childRunId: string, result: SingleResult, asyncDir: string, existing?: Record<string, unknown>, receipt?: WorkflowReceipt): Record<string, unknown> {
|
|
111
198
|
const sessionId = status.sessionId ?? (typeof existing?.sessionId === "string" ? existing.sessionId : undefined);
|
|
112
|
-
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"
|
|
113
205
|
? `Workflow completed after detached child ${childRunId} finished.`
|
|
114
|
-
: status.error ?? (typeof existing?.summary === "string" ? existing.summary :
|
|
206
|
+
: status.error ?? (typeof existing?.summary === "string" ? existing.summary : "Workflow failed.")}${workflowResultOutputPathMappingSummary(results)}`;
|
|
115
207
|
return {
|
|
116
208
|
...(existing ?? {}),
|
|
117
209
|
id: status.runId,
|
|
@@ -126,8 +218,11 @@ function publishedWorkflowResult(status: AsyncStatus, childRunId: string, result
|
|
|
126
218
|
activityState: status.activityState,
|
|
127
219
|
endedAt: status.endedAt,
|
|
128
220
|
timestamp: Date.now(),
|
|
129
|
-
results
|
|
221
|
+
results,
|
|
130
222
|
workflow: status.workflow,
|
|
223
|
+
workflowChildren: status.workflowChildren,
|
|
224
|
+
...(receipt?.terminalOutcome ? { terminalOutcome: receipt.terminalOutcome } : {}),
|
|
225
|
+
...(resolution ? { workflowResolution: resolution, recovery } : {}),
|
|
131
226
|
reconciledFromDetachedChild: childRunId,
|
|
132
227
|
...(receipt ? { workflowReceipt: { path: path.join(asyncDir, "workflow-receipt.json"), receipt } } : {}),
|
|
133
228
|
asyncDir,
|
|
@@ -137,7 +232,7 @@ function publishedWorkflowResult(status: AsyncStatus, childRunId: string, result
|
|
|
137
232
|
};
|
|
138
233
|
}
|
|
139
234
|
|
|
140
|
-
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 {
|
|
141
236
|
const receiptPath = workflowReceiptPath(DIRS.async, status.runId);
|
|
142
237
|
if (!fs.existsSync(receiptPath)) return undefined;
|
|
143
238
|
const receipt = readWorkflowReceipt(DIRS.async, status.runId);
|
|
@@ -154,6 +249,13 @@ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, resul
|
|
|
154
249
|
resumability = { state: "not-resumable", reason: error instanceof Error ? error.message : String(error) };
|
|
155
250
|
}
|
|
156
251
|
const outputReference = result.savedOutputPath ?? result.outputReference?.path ?? entry.outputReference;
|
|
252
|
+
const childStatus = readStatus(path.join(DIRS.async, childRunId));
|
|
253
|
+
const externalStep = childStatus?.steps?.length === 1 && childStatus.steps[0]?.runner?.type === "external-cli" ? childStatus.steps[0] : undefined;
|
|
254
|
+
const externalRunner = normalizeExternalCliRunnerStatus(result.runner?.type === "external-cli" ? result.runner : externalStep?.runner);
|
|
255
|
+
const externalProcess = result.externalProcess ?? externalStep?.externalProcess;
|
|
256
|
+
const externalAdapter = externalRunner ? externalCliReceiptMetadata({ runner: externalRunner, externalProcess, outputReference }) : entry.externalAdapter;
|
|
257
|
+
const childTerminalOutcome = resultTerminalOutcome(result) ?? entry.terminalOutcome;
|
|
258
|
+
if (externalAdapter) resumability = { state: "not-resumable", reason: externalAdapter.nonResumableReason };
|
|
157
259
|
const updatedEntry: WorkflowReceipt["entries"][string] = resumability.state === "resumable"
|
|
158
260
|
? {
|
|
159
261
|
...entry,
|
|
@@ -162,6 +264,8 @@ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, resul
|
|
|
162
264
|
latestRunId: entry.latestRunId ?? childRunId,
|
|
163
265
|
resumability,
|
|
164
266
|
...(outputReference ? { outputReference } : {}),
|
|
267
|
+
...(childTerminalOutcome ? { terminalOutcome: childTerminalOutcome } : {}),
|
|
268
|
+
...(externalAdapter ? { externalAdapter } : {}),
|
|
165
269
|
}
|
|
166
270
|
: {
|
|
167
271
|
...entry,
|
|
@@ -169,6 +273,8 @@ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, resul
|
|
|
169
273
|
...(step.context ? { resolvedContext: step.context } : {}),
|
|
170
274
|
resumability,
|
|
171
275
|
...(outputReference ? { outputReference } : {}),
|
|
276
|
+
...(childTerminalOutcome ? { terminalOutcome: childTerminalOutcome } : {}),
|
|
277
|
+
...(externalAdapter ? { externalAdapter } : {}),
|
|
172
278
|
};
|
|
173
279
|
const next: WorkflowReceipt = {
|
|
174
280
|
...receipt,
|
|
@@ -177,7 +283,15 @@ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, resul
|
|
|
177
283
|
...receipt.entries,
|
|
178
284
|
[key]: updatedEntry,
|
|
179
285
|
},
|
|
286
|
+
workflowChildren: status.workflowChildren,
|
|
287
|
+
...(receipt.terminalOutcome ? { terminalOutcome: receipt.terminalOutcome } : {}),
|
|
288
|
+
...(resolution ? { workflowResolution: resolution } : {}),
|
|
180
289
|
};
|
|
290
|
+
if (resolution) next.recovery = workflowRecovery(next);
|
|
291
|
+
else {
|
|
292
|
+
delete next.workflowResolution;
|
|
293
|
+
delete next.recovery;
|
|
294
|
+
}
|
|
181
295
|
writeWorkflowReceipt(asyncDir, next);
|
|
182
296
|
return next;
|
|
183
297
|
}
|
|
@@ -209,6 +323,10 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
|
|
|
209
323
|
workflowKey: input.workflowKey,
|
|
210
324
|
});
|
|
211
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;
|
|
212
330
|
writeAtomicJson(path.join(asyncDir, "status.json"), next);
|
|
213
331
|
updateActiveRunIndex(asyncDir, next.state, next.toolCallId);
|
|
214
332
|
if (job) {
|
|
@@ -228,8 +346,9 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
|
|
|
228
346
|
}
|
|
229
347
|
let receipt: WorkflowReceipt | undefined;
|
|
230
348
|
let receiptError: string | undefined;
|
|
349
|
+
const resolution = workflowResolution(next, input.result);
|
|
231
350
|
try {
|
|
232
|
-
receipt = reconcileWorkflowReceipt(next, input.childRunId, input.result, asyncDir);
|
|
351
|
+
receipt = reconcileWorkflowReceipt(next, input.childRunId, input.result, asyncDir, resolution);
|
|
233
352
|
} catch (error) {
|
|
234
353
|
receiptError = `Failed to reconcile async workflow receipt: ${error instanceof Error ? error.message : String(error)}`;
|
|
235
354
|
}
|
|
@@ -245,11 +364,15 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
|
|
|
245
364
|
});
|
|
246
365
|
}
|
|
247
366
|
if (next.state === "complete" || next.state === "failed") {
|
|
367
|
+
if (!resolution) throw new Error(`Terminal detached workflow '${input.workflowRunId}' has no resolution classification.`);
|
|
248
368
|
appendDetachedWorkflowEvent(asyncDir, {
|
|
249
369
|
ts: Date.now(),
|
|
250
370
|
runId: input.workflowRunId,
|
|
251
371
|
type: "subagent.workflow.completed",
|
|
252
372
|
state: next.state,
|
|
373
|
+
workflowResolution: resolution,
|
|
374
|
+
...(receipt?.terminalOutcome ? { terminalOutcome: receipt.terminalOutcome } : {}),
|
|
375
|
+
recovery: workflowRecovery(receipt),
|
|
253
376
|
...(next.error ? { error: next.error } : {}),
|
|
254
377
|
reconciledFromDetachedChild: input.childRunId,
|
|
255
378
|
});
|
|
@@ -261,6 +384,9 @@ export function reconcileDetachedWorkflowChildCompletion(input: {
|
|
|
261
384
|
agent: "workflow",
|
|
262
385
|
success: next.state === "complete",
|
|
263
386
|
state: next.state,
|
|
387
|
+
workflowResolution: resolution,
|
|
388
|
+
...(receipt?.terminalOutcome ? { terminalOutcome: receipt.terminalOutcome } : {}),
|
|
389
|
+
recovery: workflowRecovery(receipt),
|
|
264
390
|
summary: typeof published.summary === "string" ? published.summary : (next.state === "complete" ? "Workflow completed." : next.error),
|
|
265
391
|
reconciledFromDetachedChild: input.childRunId,
|
|
266
392
|
...(Array.isArray(published.results) ? { results: published.results } : {}),
|
|
@@ -218,6 +218,7 @@ function createPiAggregateProjection(): OversizedLineProjection {
|
|
|
218
218
|
if (!valid || token || stack.length !== 0 || rootState !== "end") return undefined;
|
|
219
219
|
if (eventType === "turn_end") return '{"type":"turn_end"}';
|
|
220
220
|
if (eventType === "agent_end" && typeof willRetry === "boolean") return JSON.stringify({ type: "agent_end", willRetry });
|
|
221
|
+
if (eventType === "compaction_end" && typeof willRetry === "boolean") return JSON.stringify({ type: "compaction_end", willRetry });
|
|
221
222
|
return undefined;
|
|
222
223
|
},
|
|
223
224
|
};
|
|
@@ -225,14 +226,15 @@ function createPiAggregateProjection(): OversizedLineProjection {
|
|
|
225
226
|
|
|
226
227
|
/**
|
|
227
228
|
* Pi JSON mode emits granular message/tool events followed by aggregate
|
|
228
|
-
* `turn_end` and `
|
|
229
|
-
* image reads can make one aggregate record exceed
|
|
230
|
-
* though every granular event was valid. Replace
|
|
231
|
-
* redundant records with the lifecycle fields the
|
|
229
|
+
* `turn_end`, `agent_end`, and `compaction_end` events that can duplicate
|
|
230
|
+
* large payloads. Parallel image reads can make one aggregate record exceed
|
|
231
|
+
* the child line limit even though every granular event was valid. Replace
|
|
232
|
+
* only syntactically valid, redundant records with the lifecycle fields the
|
|
233
|
+
* runners consume.
|
|
232
234
|
*/
|
|
233
235
|
export const PI_AGGREGATE_EVENT_PROJECTOR: OversizedLineProjector = {
|
|
234
236
|
accepts(prefix) {
|
|
235
|
-
return prefix.startsWith('{"type":"turn_end"') || prefix.startsWith('{"type":"agent_end"');
|
|
237
|
+
return prefix.startsWith('{"type":"turn_end"') || prefix.startsWith('{"type":"agent_end"') || prefix.startsWith('{"type":"compaction_end"');
|
|
236
238
|
},
|
|
237
239
|
create: createPiAggregateProjection,
|
|
238
240
|
};
|
|
@@ -393,9 +395,21 @@ export function createBoundedByteTail(maxBytes = MAX_CHILD_STDERR_BYTES): {
|
|
|
393
395
|
|
|
394
396
|
export type ChildLifecycleAction = "start-drain" | "cancel-drain" | "none";
|
|
395
397
|
|
|
396
|
-
export
|
|
398
|
+
export interface ChildLifecycleState {
|
|
399
|
+
compactionRetryActive: boolean;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function projectChildLifecycle(event: { type?: string; willRetry?: unknown }, terminalAssistantStop = false, state?: ChildLifecycleState): ChildLifecycleAction {
|
|
403
|
+
if (event.type === "compaction_end") {
|
|
404
|
+
if (state) state.compactionRetryActive = event.willRetry === true;
|
|
405
|
+
return event.willRetry === true ? "cancel-drain" : "none";
|
|
406
|
+
}
|
|
407
|
+
if (event.type === "agent_start" || event.type === "auto_retry_start") {
|
|
408
|
+
if (state) state.compactionRetryActive = false;
|
|
409
|
+
}
|
|
397
410
|
if (event.type === "agent_end" && event.willRetry === true) return "cancel-drain";
|
|
398
|
-
if (event.type === "
|
|
411
|
+
if (event.type === "agent_end" && state) state.compactionRetryActive = false;
|
|
412
|
+
if (event.type === "agent_settled") return state?.compactionRetryActive ? "none" : "start-drain";
|
|
399
413
|
if (terminalAssistantStop) return "start-drain";
|
|
400
414
|
return "none";
|
|
401
415
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { parseExternalCliJsonlEvent, type ExternalCliParser, type ExternalCliParserProgress, type ExternalCliParserTerminal } from "./external-cli-runner.ts";
|
|
2
|
+
import type { ExternalCliPreflightSpec } from "./external-cli-preflight.ts";
|
|
3
|
+
|
|
4
|
+
const MAX_EVENT_TYPE_LENGTH = 128;
|
|
5
|
+
const MAX_ERROR_LENGTH = 4_096;
|
|
6
|
+
|
|
7
|
+
export const CLAUDE_CODE_ADAPTER_ID = "claude-code" as const;
|
|
8
|
+
export const CLAUDE_CODE_WRITER_ADAPTER_ID = "claude-code-writer" as const;
|
|
9
|
+
export const CLAUDE_CODE_WRITER_TOOLS = "Read,Write,Edit,Glob,Grep" as const;
|
|
10
|
+
export const CLAUDE_CODE_ENV_ALLOWLIST = [
|
|
11
|
+
"PATH",
|
|
12
|
+
"HOME",
|
|
13
|
+
"USERPROFILE",
|
|
14
|
+
"USER",
|
|
15
|
+
"LOGNAME",
|
|
16
|
+
"TMPDIR",
|
|
17
|
+
"CLAUDE_CONFIG_DIR",
|
|
18
|
+
"ANTHROPIC_API_KEY",
|
|
19
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
20
|
+
"ANTHROPIC_BASE_URL",
|
|
21
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
22
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
23
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
24
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
25
|
+
"AWS_PROFILE",
|
|
26
|
+
"AWS_REGION",
|
|
27
|
+
"AWS_DEFAULT_REGION",
|
|
28
|
+
"AWS_ACCESS_KEY_ID",
|
|
29
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
30
|
+
"AWS_SESSION_TOKEN",
|
|
31
|
+
"AWS_BEARER_TOKEN_BEDROCK",
|
|
32
|
+
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
33
|
+
"CLOUD_ML_REGION",
|
|
34
|
+
"ANTHROPIC_VERTEX_PROJECT_ID",
|
|
35
|
+
"HTTP_PROXY",
|
|
36
|
+
"HTTPS_PROXY",
|
|
37
|
+
"NO_PROXY",
|
|
38
|
+
"http_proxy",
|
|
39
|
+
"https_proxy",
|
|
40
|
+
"no_proxy",
|
|
41
|
+
"SSL_CERT_FILE",
|
|
42
|
+
"SSL_CERT_DIR",
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
function terminalError(event: Record<string, unknown>): string {
|
|
46
|
+
for (const value of [event.error, event.result]) {
|
|
47
|
+
if (typeof value === "string" && value.trim()) return value.trim().slice(0, MAX_ERROR_LENGTH);
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(event.errors)) {
|
|
50
|
+
const messages = event.errors.filter((value): value is string => typeof value === "string" && Boolean(value.trim()));
|
|
51
|
+
if (messages.length > 0) return messages.join("; ").slice(0, MAX_ERROR_LENGTH);
|
|
52
|
+
}
|
|
53
|
+
const subtype = typeof event.subtype === "string" && event.subtype ? event.subtype : "unknown";
|
|
54
|
+
return `Claude Code reported terminal result ${subtype}.`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createClaudeCodeJsonlParser(): ExternalCliParser {
|
|
58
|
+
let eventCount = 0;
|
|
59
|
+
let terminal: ExternalCliParserTerminal | undefined;
|
|
60
|
+
return {
|
|
61
|
+
parseLine(line): ExternalCliParserProgress {
|
|
62
|
+
const event = parseExternalCliJsonlEvent(line, "Claude Code", MAX_EVENT_TYPE_LENGTH);
|
|
63
|
+
if (terminal && event.type === "result") throw new Error("Claude Code emitted a duplicate terminal result.");
|
|
64
|
+
eventCount += 1;
|
|
65
|
+
if (!terminal && event.type === "result") {
|
|
66
|
+
if (event.subtype === "success" && event.is_error === false && typeof event.result === "string" && event.result.trim()) {
|
|
67
|
+
terminal = { state: "completed", output: event.result.trim() };
|
|
68
|
+
} else {
|
|
69
|
+
terminal = { state: "failed", error: terminalError(event) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { phase: terminal ? terminal.state : "streaming", eventCount };
|
|
73
|
+
},
|
|
74
|
+
finish(): ExternalCliParserTerminal | undefined {
|
|
75
|
+
return terminal;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveClaudeCodeLaunch(input: {
|
|
81
|
+
adapter: typeof CLAUDE_CODE_ADAPTER_ID | typeof CLAUDE_CODE_WRITER_ADAPTER_ID;
|
|
82
|
+
command: string;
|
|
83
|
+
/** Test-only executable prefix for a fake Claude Code process. */
|
|
84
|
+
commandPrefixArgs?: readonly string[];
|
|
85
|
+
}): {
|
|
86
|
+
command: string;
|
|
87
|
+
args: string[];
|
|
88
|
+
finalOutputPath?: undefined;
|
|
89
|
+
promptFilePath?: undefined;
|
|
90
|
+
temporaryDirectories?: undefined;
|
|
91
|
+
environment: { allowlist: readonly string[] };
|
|
92
|
+
preflight: ExternalCliPreflightSpec;
|
|
93
|
+
parser: ExternalCliParser;
|
|
94
|
+
} {
|
|
95
|
+
const writer = input.adapter === CLAUDE_CODE_WRITER_ADAPTER_ID;
|
|
96
|
+
const prefix = [...(input.commandPrefixArgs ?? [])];
|
|
97
|
+
const args = [
|
|
98
|
+
...prefix,
|
|
99
|
+
"-p",
|
|
100
|
+
"--input-format", "text",
|
|
101
|
+
"--output-format", "stream-json",
|
|
102
|
+
"--verbose",
|
|
103
|
+
"--permission-mode", writer ? "acceptEdits" : "plan",
|
|
104
|
+
"--tools", writer ? CLAUDE_CODE_WRITER_TOOLS : "",
|
|
105
|
+
"--strict-mcp-config",
|
|
106
|
+
"--mcp-config", '{"mcpServers":{}}',
|
|
107
|
+
"--setting-sources", "user",
|
|
108
|
+
"--no-session-persistence",
|
|
109
|
+
"--disable-slash-commands",
|
|
110
|
+
"--no-chrome",
|
|
111
|
+
];
|
|
112
|
+
return {
|
|
113
|
+
command: input.command,
|
|
114
|
+
args,
|
|
115
|
+
environment: { allowlist: CLAUDE_CODE_ENV_ALLOWLIST },
|
|
116
|
+
preflight: {
|
|
117
|
+
id: input.adapter,
|
|
118
|
+
versionArgs: [...prefix, "--version"],
|
|
119
|
+
helpArgs: [...prefix, "--help"],
|
|
120
|
+
validate(result) {
|
|
121
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)? \(Claude Code\)$/.test(result.version)) throw new Error(`Unsupported Claude Code version response: ${JSON.stringify(result.version)}.`);
|
|
122
|
+
for (const required of ["Claude Code - starts an interactive session", "--print", "--input-format", "stream-json", "--verbose", "--permission-mode", writer ? "acceptEdits" : "plan", "--tools", "--strict-mcp-config", "--mcp-config", "--setting-sources", "--no-session-persistence", "--disable-slash-commands", "--no-chrome"]) {
|
|
123
|
+
if (!result.help.includes(required)) throw new Error(`Claude Code help does not document required option ${JSON.stringify(required)}.`);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
parser: createClaudeCodeJsonlParser(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { parseExternalCliJsonlEvent, type ExternalCliParser, type ExternalCliParserProgress, type ExternalCliParserTerminal } from "./external-cli-runner.ts";
|
|
4
|
+
import type { ExternalCliPreflightSpec } from "./external-cli-preflight.ts";
|
|
5
|
+
|
|
6
|
+
const MAX_FINAL_MESSAGE_BYTES = 1024 * 1024;
|
|
7
|
+
const MAX_EVENT_TYPE_LENGTH = 128;
|
|
8
|
+
|
|
9
|
+
export const CODEX_EXEC_ADAPTER_ID = "codex-exec" as const;
|
|
10
|
+
export const CODEX_EXEC_WRITER_ADAPTER_ID = "codex-exec-writer" as const;
|
|
11
|
+
export const CODEX_EXEC_ENV_ALLOWLIST = [
|
|
12
|
+
"PATH",
|
|
13
|
+
"HOME",
|
|
14
|
+
"USERPROFILE",
|
|
15
|
+
"CODEX_HOME",
|
|
16
|
+
"CODEX_API_KEY",
|
|
17
|
+
"OPENAI_API_KEY",
|
|
18
|
+
"HTTP_PROXY",
|
|
19
|
+
"HTTPS_PROXY",
|
|
20
|
+
"NO_PROXY",
|
|
21
|
+
"http_proxy",
|
|
22
|
+
"https_proxy",
|
|
23
|
+
"no_proxy",
|
|
24
|
+
"SSL_CERT_FILE",
|
|
25
|
+
"SSL_CERT_DIR",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
function eventError(event: Record<string, unknown>, fallback: string): string {
|
|
29
|
+
const error = event.error;
|
|
30
|
+
if (typeof error === "string" && error.trim()) return error.trim().slice(0, 4_096);
|
|
31
|
+
if (error && typeof error === "object" && !Array.isArray(error)) {
|
|
32
|
+
const message = (error as Record<string, unknown>).message;
|
|
33
|
+
if (typeof message === "string" && message.trim()) return message.trim().slice(0, 4_096);
|
|
34
|
+
}
|
|
35
|
+
const message = event.message;
|
|
36
|
+
return typeof message === "string" && message.trim() ? message.trim().slice(0, 4_096) : fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createCodexExecJsonlParser(finalMessagePath: string): ExternalCliParser {
|
|
40
|
+
let eventCount = 0;
|
|
41
|
+
let terminal: ExternalCliParserTerminal | undefined;
|
|
42
|
+
return {
|
|
43
|
+
parseLine(line): ExternalCliParserProgress {
|
|
44
|
+
const event = parseExternalCliJsonlEvent(line, "Codex exec", MAX_EVENT_TYPE_LENGTH);
|
|
45
|
+
if (terminal) throw new Error("Codex exec emitted an event after its terminal state.");
|
|
46
|
+
eventCount += 1;
|
|
47
|
+
if (event.type === "turn.completed") terminal = { state: "completed" };
|
|
48
|
+
else if (event.type === "turn.failed") terminal = { state: "failed", error: eventError(event, "Codex exec reported turn.failed.") };
|
|
49
|
+
else if (event.type === "error") terminal = { state: "failed", error: eventError(event, "Codex exec reported an error event.") };
|
|
50
|
+
return { phase: terminal ? terminal.state : "streaming", eventCount };
|
|
51
|
+
},
|
|
52
|
+
finish(): ExternalCliParserTerminal | undefined {
|
|
53
|
+
if (!terminal || terminal.state === "failed") return terminal;
|
|
54
|
+
let descriptor: number;
|
|
55
|
+
try { descriptor = fs.openSync(finalMessagePath, "r"); }
|
|
56
|
+
catch (error) { throw new Error(`Codex exec did not write its final-message artifact: ${error instanceof Error ? error.message : String(error)}`); }
|
|
57
|
+
try {
|
|
58
|
+
const stat = fs.fstatSync(descriptor);
|
|
59
|
+
if (!stat.isFile()) throw new Error("Codex exec final-message artifact is not a file.");
|
|
60
|
+
if (stat.size > MAX_FINAL_MESSAGE_BYTES) throw new Error("Codex exec final-message artifact exceeded its byte limit.");
|
|
61
|
+
const content = Buffer.alloc(stat.size);
|
|
62
|
+
let bytesRead = 0;
|
|
63
|
+
while (bytesRead < content.length) {
|
|
64
|
+
const count = fs.readSync(descriptor, content, bytesRead, content.length - bytesRead, bytesRead);
|
|
65
|
+
if (count === 0) break;
|
|
66
|
+
bytesRead += count;
|
|
67
|
+
}
|
|
68
|
+
const output = content.subarray(0, bytesRead).toString("utf-8").trim();
|
|
69
|
+
if (!output) throw new Error("Codex exec final-message artifact is empty.");
|
|
70
|
+
return { state: "completed", output };
|
|
71
|
+
} finally { fs.closeSync(descriptor); }
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resolveCodexExecLaunch(input: {
|
|
77
|
+
adapter: typeof CODEX_EXEC_ADAPTER_ID | typeof CODEX_EXEC_WRITER_ADAPTER_ID;
|
|
78
|
+
command: string;
|
|
79
|
+
asyncDir: string;
|
|
80
|
+
stepIndex: number;
|
|
81
|
+
/** Test-only executable prefix for a fake Codex process. */
|
|
82
|
+
commandPrefixArgs?: readonly string[];
|
|
83
|
+
}): {
|
|
84
|
+
command: string;
|
|
85
|
+
args: string[];
|
|
86
|
+
finalOutputPath: string;
|
|
87
|
+
promptFilePath?: undefined;
|
|
88
|
+
temporaryDirectories?: undefined;
|
|
89
|
+
environment: { allowlist: readonly string[] };
|
|
90
|
+
preflight: ExternalCliPreflightSpec;
|
|
91
|
+
parser: ExternalCliParser;
|
|
92
|
+
} {
|
|
93
|
+
const writer = input.adapter === CODEX_EXEC_WRITER_ADAPTER_ID;
|
|
94
|
+
const finalMessagePath = path.join(input.asyncDir, `external-${input.stepIndex}.final-message.txt`);
|
|
95
|
+
fs.rmSync(finalMessagePath, { force: true });
|
|
96
|
+
const prefix = [...(input.commandPrefixArgs ?? [])];
|
|
97
|
+
const args = [
|
|
98
|
+
...prefix,
|
|
99
|
+
"exec",
|
|
100
|
+
"--json",
|
|
101
|
+
"--color", "never",
|
|
102
|
+
"--ephemeral",
|
|
103
|
+
"--ignore-user-config",
|
|
104
|
+
"--ignore-rules",
|
|
105
|
+
"--skip-git-repo-check",
|
|
106
|
+
"-s", writer ? "workspace-write" : "read-only",
|
|
107
|
+
"-c", 'approval_policy="never"',
|
|
108
|
+
"--output-last-message", finalMessagePath,
|
|
109
|
+
"-",
|
|
110
|
+
];
|
|
111
|
+
return {
|
|
112
|
+
command: input.command,
|
|
113
|
+
args,
|
|
114
|
+
finalOutputPath: finalMessagePath,
|
|
115
|
+
environment: { allowlist: CODEX_EXEC_ENV_ALLOWLIST },
|
|
116
|
+
preflight: {
|
|
117
|
+
id: input.adapter,
|
|
118
|
+
versionArgs: [...prefix, "--version"],
|
|
119
|
+
helpArgs: [...prefix, "exec", "--help"],
|
|
120
|
+
validate(result) {
|
|
121
|
+
if (!/^codex-cli \d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(result.version)) throw new Error(`Unsupported Codex version response: ${JSON.stringify(result.version)}.`);
|
|
122
|
+
for (const required of ["Run Codex non-interactively", "--json", "--output-last-message", "--ephemeral", "--ignore-user-config", "--ignore-rules", "--skip-git-repo-check", "--sandbox", writer ? "workspace-write" : "read-only", "--config"]) {
|
|
123
|
+
if (!result.help.includes(required)) throw new Error(`Codex exec help does not document required option ${JSON.stringify(required)}.`);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
parser: createCodexExecJsonlParser(finalMessagePath),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -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 {
|