pi-subagents 0.56.0 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -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 +111 -15
- package/docs/configuration.md +37 -0
- package/docs/extension-api.md +2 -2
- package/docs/observability.md +8 -7
- package/docs/tool-reference.md +11 -1
- package/docs/workflows.md +20 -0
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +1 -1
- package/src/agents/agent-management.ts +13 -5
- package/src/agents/agent-refinements.ts +4 -4
- package/src/agents/agents.ts +219 -48
- package/src/agents/builtin-names.ts +6 -0
- package/src/agents/runtime-agent-registry.ts +8 -2
- package/src/api/preflight.ts +2 -2
- package/src/extension/config.ts +70 -0
- package/src/extension/doctor.ts +3 -3
- package/src/extension/index.ts +18 -4
- package/src/extension/public-execution.ts +29 -13
- package/src/extension/rpc.ts +20 -5
- package/src/extension/schemas.ts +4 -3
- package/src/extension/tool-description.ts +11 -9
- 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 +46 -3
- package/src/runs/background/active-async-capacity.ts +77 -10
- package/src/runs/background/async-execution.ts +100 -9
- package/src/runs/background/async-job-tracker.ts +5 -0
- package/src/runs/background/async-retention.ts +20 -3
- package/src/runs/background/async-status.ts +5 -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/result-files.ts +27 -14
- package/src/runs/background/result-watcher.ts +9 -5
- 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 +177 -44
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/execution.ts +72 -10
- 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 +270 -81
- package/src/runs/foreground/workflow-detach-reconcile.ts +16 -1
- 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/cursor-agent-adapter.ts +114 -0
- 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/model-exclusions.ts +57 -7
- package/src/runs/shared/model-fallback.ts +23 -1
- 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/process-signal.ts +13 -0
- package/src/runs/shared/run-history.ts +21 -1
- package/src/shared/fork-context.ts +21 -0
- package/src/shared/formatters.ts +7 -1
- 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 +118 -9
- 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/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 +47 -21
- package/src/workflows/scripted-workflow.ts +248 -27
- package/src/workflows/workflow-child-summary.ts +117 -0
- package/src/workflows/workflow-receipt.ts +119 -4
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
import { updateActiveRunIndex } from "../background/active-run-index.ts";
|
|
14
14
|
import { resultFilePath, writeAsyncResultFile } from "../background/result-files.ts";
|
|
15
15
|
import { resolveAsyncResumeTarget } from "../background/async-resume.ts";
|
|
16
|
+
import { externalCliReceiptMetadata, normalizeExternalCliRunnerStatus } from "../shared/external-cli-contract.ts";
|
|
16
17
|
import { readWorkflowReceipt, workflowReceiptPath, writeWorkflowReceipt, type WorkflowReceipt } from "../../workflows/workflow-receipt.ts";
|
|
18
|
+
import { workflowChildSummary } from "../../workflows/workflow-child-summary.ts";
|
|
17
19
|
|
|
18
20
|
function cloneWorkflowStatus(status: AsyncStatus): AsyncStatus {
|
|
19
21
|
return {
|
|
@@ -55,7 +57,10 @@ export function applyDetachedChildToPausedWorkflow(
|
|
|
55
57
|
const promoted = promotePausedWorkflowIfSettled(next);
|
|
56
58
|
if (promoted?.state === "failed" && input.result.interrupted && !failedSiblingError) promoted.error = input.result.error ?? INTERRUPTED_DETACHED_CHILD;
|
|
57
59
|
else if (promoted?.state === "failed" && input.result.error) promoted.error = input.result.error;
|
|
58
|
-
|
|
60
|
+
const resolved = promoted ?? next;
|
|
61
|
+
const workflowState = resolved.state === "complete" ? "completed" : resolved.state === "paused" ? "paused" : resolved.state === "stopped" ? "stopped" : "failed";
|
|
62
|
+
resolved.workflowChildren = workflowChildSummary({ parentToolCallId: resolved.toolCallId ?? resolved.runId, workflowRunId: resolved.runId, workflowState, inventoryComplete: true, trace: resolved.workflow?.trace, steps: resolved.steps });
|
|
63
|
+
return resolved;
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
export function promotePausedWorkflowIfSettled(status: AsyncStatus): AsyncStatus | undefined {
|
|
@@ -128,6 +133,7 @@ function publishedWorkflowResult(status: AsyncStatus, childRunId: string, result
|
|
|
128
133
|
timestamp: Date.now(),
|
|
129
134
|
results: workflowResultChildren(status, childRunId, result, existing?.results),
|
|
130
135
|
workflow: status.workflow,
|
|
136
|
+
workflowChildren: status.workflowChildren,
|
|
131
137
|
reconciledFromDetachedChild: childRunId,
|
|
132
138
|
...(receipt ? { workflowReceipt: { path: path.join(asyncDir, "workflow-receipt.json"), receipt } } : {}),
|
|
133
139
|
asyncDir,
|
|
@@ -154,6 +160,12 @@ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, resul
|
|
|
154
160
|
resumability = { state: "not-resumable", reason: error instanceof Error ? error.message : String(error) };
|
|
155
161
|
}
|
|
156
162
|
const outputReference = result.savedOutputPath ?? result.outputReference?.path ?? entry.outputReference;
|
|
163
|
+
const childStatus = readStatus(path.join(DIRS.async, childRunId));
|
|
164
|
+
const externalStep = childStatus?.steps?.length === 1 && childStatus.steps[0]?.runner?.type === "external-cli" ? childStatus.steps[0] : undefined;
|
|
165
|
+
const externalRunner = normalizeExternalCliRunnerStatus(result.runner?.type === "external-cli" ? result.runner : externalStep?.runner);
|
|
166
|
+
const externalProcess = result.externalProcess ?? externalStep?.externalProcess;
|
|
167
|
+
const externalAdapter = externalRunner ? externalCliReceiptMetadata({ runner: externalRunner, externalProcess, outputReference }) : entry.externalAdapter;
|
|
168
|
+
if (externalAdapter) resumability = { state: "not-resumable", reason: externalAdapter.nonResumableReason };
|
|
157
169
|
const updatedEntry: WorkflowReceipt["entries"][string] = resumability.state === "resumable"
|
|
158
170
|
? {
|
|
159
171
|
...entry,
|
|
@@ -162,6 +174,7 @@ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, resul
|
|
|
162
174
|
latestRunId: entry.latestRunId ?? childRunId,
|
|
163
175
|
resumability,
|
|
164
176
|
...(outputReference ? { outputReference } : {}),
|
|
177
|
+
...(externalAdapter ? { externalAdapter } : {}),
|
|
165
178
|
}
|
|
166
179
|
: {
|
|
167
180
|
...entry,
|
|
@@ -169,6 +182,7 @@ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, resul
|
|
|
169
182
|
...(step.context ? { resolvedContext: step.context } : {}),
|
|
170
183
|
resumability,
|
|
171
184
|
...(outputReference ? { outputReference } : {}),
|
|
185
|
+
...(externalAdapter ? { externalAdapter } : {}),
|
|
172
186
|
};
|
|
173
187
|
const next: WorkflowReceipt = {
|
|
174
188
|
...receipt,
|
|
@@ -177,6 +191,7 @@ function reconcileWorkflowReceipt(status: AsyncStatus, childRunId: string, resul
|
|
|
177
191
|
...receipt.entries,
|
|
178
192
|
[key]: updatedEntry,
|
|
179
193
|
},
|
|
194
|
+
workflowChildren: status.workflowChildren,
|
|
180
195
|
};
|
|
181
196
|
writeWorkflowReceipt(asyncDir, next);
|
|
182
197
|
return next;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { parseExternalCliJsonlEvent, type ExternalCliParser, type ExternalCliParserProgress, type ExternalCliParserTerminal } from "./external-cli-runner.ts";
|
|
3
|
+
import type { ExternalCliPreflightSpec } from "./external-cli-preflight.ts";
|
|
4
|
+
|
|
5
|
+
const MAX_EVENT_TYPE_LENGTH = 128;
|
|
6
|
+
const MAX_ERROR_LENGTH = 4_096;
|
|
7
|
+
const MAX_OVERSIZED_TOOL_CALL_BYTES = 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
export const CURSOR_AGENT_ADAPTER_ID = "cursor-agent" as const;
|
|
10
|
+
export const CURSOR_AGENT_WRITER_ADAPTER_ID = "cursor-agent-writer" as const;
|
|
11
|
+
export const CURSOR_AGENT_ENV_ALLOWLIST = [
|
|
12
|
+
"PATH",
|
|
13
|
+
"HOME",
|
|
14
|
+
"USERPROFILE",
|
|
15
|
+
"CURSOR_API_KEY",
|
|
16
|
+
"HTTP_PROXY",
|
|
17
|
+
"HTTPS_PROXY",
|
|
18
|
+
"NO_PROXY",
|
|
19
|
+
"http_proxy",
|
|
20
|
+
"https_proxy",
|
|
21
|
+
"no_proxy",
|
|
22
|
+
"SSL_CERT_FILE",
|
|
23
|
+
"SSL_CERT_DIR",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
function terminalError(event: Record<string, unknown>): string {
|
|
27
|
+
for (const value of [event.result, event.error, event.message]) {
|
|
28
|
+
if (typeof value === "string" && value.trim()) return value.trim().slice(0, MAX_ERROR_LENGTH);
|
|
29
|
+
}
|
|
30
|
+
const subtype = typeof event.subtype === "string" && event.subtype ? event.subtype : "unknown";
|
|
31
|
+
return `Cursor Agent reported terminal result ${subtype}.`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createCursorAgentJsonlParser(): ExternalCliParser {
|
|
35
|
+
let eventCount = 0;
|
|
36
|
+
let terminal: ExternalCliParserTerminal | undefined;
|
|
37
|
+
return {
|
|
38
|
+
parseLine(line): ExternalCliParserProgress {
|
|
39
|
+
const event = parseExternalCliJsonlEvent(line, "Cursor Agent", MAX_EVENT_TYPE_LENGTH);
|
|
40
|
+
if (terminal) throw new Error("Cursor Agent emitted an event after its terminal state.");
|
|
41
|
+
eventCount += 1;
|
|
42
|
+
if (event.type === "error") terminal = { state: "failed", error: terminalError(event) };
|
|
43
|
+
else if (event.type === "result") {
|
|
44
|
+
if (event.subtype === "success" && event.is_error === false && typeof event.result === "string" && event.result.trim()) {
|
|
45
|
+
terminal = { state: "completed", output: event.result.trim() };
|
|
46
|
+
} else terminal = { state: "failed", error: terminalError(event) };
|
|
47
|
+
}
|
|
48
|
+
return { phase: terminal ? terminal.state : "streaming", eventCount };
|
|
49
|
+
},
|
|
50
|
+
skipOversizedLine(prefix, byteLength): ExternalCliParserProgress | undefined {
|
|
51
|
+
if (terminal || byteLength > MAX_OVERSIZED_TOOL_CALL_BYTES || !/^\s*\{\s*"type"\s*:\s*"tool_call"\s*,/.test(prefix)) return undefined;
|
|
52
|
+
eventCount += 1;
|
|
53
|
+
return { phase: "streaming", eventCount };
|
|
54
|
+
},
|
|
55
|
+
finish(): ExternalCliParserTerminal | undefined {
|
|
56
|
+
return terminal;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveCursorAgentLaunch(input: {
|
|
62
|
+
adapter: typeof CURSOR_AGENT_ADAPTER_ID | typeof CURSOR_AGENT_WRITER_ADAPTER_ID;
|
|
63
|
+
command: string;
|
|
64
|
+
cwd: string;
|
|
65
|
+
asyncDir: string;
|
|
66
|
+
stepIndex: number;
|
|
67
|
+
/** Test-only executable prefix for a fake Cursor Agent process. */
|
|
68
|
+
commandPrefixArgs?: readonly string[];
|
|
69
|
+
}): {
|
|
70
|
+
command: string;
|
|
71
|
+
args: string[];
|
|
72
|
+
finalOutputPath?: undefined;
|
|
73
|
+
promptFilePath: string;
|
|
74
|
+
temporaryDirectories: string[];
|
|
75
|
+
environment: { allowlist: readonly string[] };
|
|
76
|
+
preflight: ExternalCliPreflightSpec;
|
|
77
|
+
parser: ExternalCliParser;
|
|
78
|
+
} {
|
|
79
|
+
const writer = input.adapter === CURSOR_AGENT_WRITER_ADAPTER_ID;
|
|
80
|
+
const promptDirectory = path.join(input.asyncDir, `external-${input.stepIndex}.cursor-prompt`);
|
|
81
|
+
const promptFilePath = path.join(promptDirectory, "handoff.txt");
|
|
82
|
+
const promptRelative = path.relative(input.cwd, promptDirectory);
|
|
83
|
+
const promptOutsideWorkspace = promptRelative.startsWith("..") || path.isAbsolute(promptRelative);
|
|
84
|
+
const prefix = [...(input.commandPrefixArgs ?? [])];
|
|
85
|
+
const args = [
|
|
86
|
+
...prefix,
|
|
87
|
+
"-p",
|
|
88
|
+
"--output-format", "stream-json",
|
|
89
|
+
...(writer ? [] : ["--mode", "ask"]),
|
|
90
|
+
"--sandbox", "enabled",
|
|
91
|
+
"--workspace", input.cwd,
|
|
92
|
+
...(promptOutsideWorkspace ? ["--add-dir", promptDirectory] : []),
|
|
93
|
+
`Read the complete handoff from the private file at ${promptFilePath}. Follow it and return only the final answer.`,
|
|
94
|
+
];
|
|
95
|
+
return {
|
|
96
|
+
command: input.command,
|
|
97
|
+
args,
|
|
98
|
+
promptFilePath,
|
|
99
|
+
temporaryDirectories: [promptDirectory],
|
|
100
|
+
environment: { allowlist: CURSOR_AGENT_ENV_ALLOWLIST },
|
|
101
|
+
preflight: {
|
|
102
|
+
id: input.adapter,
|
|
103
|
+
versionArgs: [...prefix, "--version"],
|
|
104
|
+
helpArgs: [...prefix, "--help"],
|
|
105
|
+
validate(result) {
|
|
106
|
+
if (!/^\d{4}\.\d{2}\.\d{2}-[0-9a-f]+$/.test(result.version)) throw new Error(`Unsupported Cursor Agent version response: ${JSON.stringify(result.version)}.`);
|
|
107
|
+
for (const required of ["Start the Cursor Agent", "--print", "stream-json", "--sandbox <mode>", "enabled", "--workspace <path-or-name>", "--add-dir <path>", ...(writer ? ["all tools", "including write and shell"] : ["--mode <mode>", "ask:", "read-only"])]) {
|
|
108
|
+
if (!result.help.includes(required)) throw new Error(`Cursor Agent help does not document required option ${JSON.stringify(required)}.`);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
parser: createCursorAgentJsonlParser(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExternalCliReceiptMetadata,
|
|
3
|
+
ExternalCliCapabilityNarrowing,
|
|
4
|
+
ExternalCliRunnerStatus,
|
|
5
|
+
ExternalProcessStatus,
|
|
6
|
+
} from "../../shared/types.ts";
|
|
7
|
+
|
|
8
|
+
const UNSUPPORTED = {
|
|
9
|
+
steer: "The one-shot stdin adapter closes input after launch and cannot accept live steer messages.",
|
|
10
|
+
resume: "The one-shot stdin adapter has no durable external session identity.",
|
|
11
|
+
structuredOutput: "The generic external CLI adapter does not parse a trusted structured result.",
|
|
12
|
+
toolEvents: "The generic external CLI adapter treats stdout as untrusted text, not native Pi tool events.",
|
|
13
|
+
supervisor: "The generic external CLI adapter has no trusted supervisor event transport.",
|
|
14
|
+
forkContext: "Native Pi fork context is not available without an adapter-owned handoff artifact.",
|
|
15
|
+
extensionBindings: "Native Pi extension bindings are never passed to external runners.",
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
const CAPABILITY_KEYS = new Set(Object.keys(UNSUPPORTED));
|
|
19
|
+
const PROMPT_FILE_UNSUPPORTED = {
|
|
20
|
+
...UNSUPPORTED,
|
|
21
|
+
steer: "The one-shot prompt-file adapter closes input after launch and cannot accept live steer messages.",
|
|
22
|
+
resume: "The one-shot prompt-file adapter does not retain a durable external session identity.",
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
export const CODE_OWNED_EXTERNAL_CLI_ADAPTER_IDS = [
|
|
26
|
+
"codex-exec",
|
|
27
|
+
"codex-exec-writer",
|
|
28
|
+
"claude-code",
|
|
29
|
+
"claude-code-writer",
|
|
30
|
+
"cursor-agent",
|
|
31
|
+
"cursor-agent-writer",
|
|
32
|
+
] as const;
|
|
33
|
+
export type CodeOwnedExternalCliAdapterId = typeof CODE_OWNED_EXTERNAL_CLI_ADAPTER_IDS[number];
|
|
34
|
+
|
|
35
|
+
const CODE_OWNED_EXTERNAL_CLI_ADAPTER_ID_SET = new Set<string>(CODE_OWNED_EXTERNAL_CLI_ADAPTER_IDS);
|
|
36
|
+
export const CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL = CODE_OWNED_EXTERNAL_CLI_ADAPTER_IDS.map((id) => `'${id}'`).join(", ");
|
|
37
|
+
|
|
38
|
+
export function isCodeOwnedExternalCliAdapterId(value: unknown): value is CodeOwnedExternalCliAdapterId {
|
|
39
|
+
return typeof value === "string" && CODE_OWNED_EXTERNAL_CLI_ADAPTER_ID_SET.has(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const RESERVED_READ_ONLY_ADAPTERS = [
|
|
43
|
+
{ name: "claude-code", writer: "claude-code-writer", access: "file-write" },
|
|
44
|
+
{ name: "codex-exec", writer: "codex-exec-writer", access: "workspace-write" },
|
|
45
|
+
{ name: "cursor-agent", writer: "cursor-agent-writer", access: "workspace-write" },
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
export function validateCodeOwnedProfileRunner(
|
|
49
|
+
agent: {
|
|
50
|
+
name: string;
|
|
51
|
+
localName?: string;
|
|
52
|
+
aliases?: readonly string[];
|
|
53
|
+
runner?: { type: string; adapter?: string };
|
|
54
|
+
},
|
|
55
|
+
): string | undefined {
|
|
56
|
+
const selectionNames = [agent.name, ...(agent.localName ? [agent.localName] : []), ...(agent.aliases ?? [])];
|
|
57
|
+
for (const adapter of RESERVED_READ_ONLY_ADAPTERS) {
|
|
58
|
+
if (selectionNames.includes(adapter.name) && !(agent.runner?.type === "external-cli" && agent.runner.adapter === adapter.name)) {
|
|
59
|
+
return `Selection name '${adapter.name}' is reserved for the read-only '${adapter.name}' adapter. Use '${adapter.writer}' for explicit ${adapter.access} access.`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseExternalCliCapabilityNarrowing(value: unknown, label: string): ExternalCliCapabilityNarrowing | undefined {
|
|
66
|
+
if (value === undefined) return undefined;
|
|
67
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
|
|
68
|
+
const input = value as Record<string, unknown>;
|
|
69
|
+
const unknown = Object.keys(input).filter((key) => !CAPABILITY_KEYS.has(key));
|
|
70
|
+
if (unknown.length > 0) throw new Error(`${label} has unsupported fields: ${unknown.join(", ")}.`);
|
|
71
|
+
for (const [key, setting] of Object.entries(input)) {
|
|
72
|
+
if (setting !== false) throw new Error(`${label}.${key} may only be false; user config cannot widen code-owned external adapter capabilities.`);
|
|
73
|
+
}
|
|
74
|
+
return input as ExternalCliCapabilityNarrowing;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function resolveExternalCliRunnerStatus(input: {
|
|
78
|
+
adapter?: CodeOwnedExternalCliAdapterId;
|
|
79
|
+
command: string;
|
|
80
|
+
args?: string[];
|
|
81
|
+
promptDelivery?: "stdin";
|
|
82
|
+
capabilities?: ExternalCliCapabilityNarrowing;
|
|
83
|
+
}): ExternalCliRunnerStatus {
|
|
84
|
+
const codexExec = input.adapter === "codex-exec";
|
|
85
|
+
const codexExecWriter = input.adapter === "codex-exec-writer";
|
|
86
|
+
const claudeCode = input.adapter === "claude-code";
|
|
87
|
+
const claudeCodeWriter = input.adapter === "claude-code-writer";
|
|
88
|
+
const cursorAgent = input.adapter === "cursor-agent";
|
|
89
|
+
const cursorAgentWriter = input.adapter === "cursor-agent-writer";
|
|
90
|
+
const cursor = cursorAgent || cursorAgentWriter;
|
|
91
|
+
const unsupported = cursor ? PROMPT_FILE_UNSUPPORTED : UNSUPPORTED;
|
|
92
|
+
return {
|
|
93
|
+
type: "external-cli",
|
|
94
|
+
command: input.command,
|
|
95
|
+
args: input.args ?? [],
|
|
96
|
+
promptDelivery: cursor ? "prompt-file" : input.promptDelivery ?? "stdin",
|
|
97
|
+
adapter: { id: input.adapter ?? "external-cli", version: 1, executionMode: cursor ? "one-shot-prompt-file" : "one-shot-stdin" },
|
|
98
|
+
...(codexExec ? { safety: { sandbox: "read-only" as const, approvalPolicy: "never" as const, ephemeral: true as const } } : {}),
|
|
99
|
+
...(codexExecWriter ? { safety: { access: "workspace-write" as const, sandbox: "workspace-write" as const, approvalPolicy: "never" as const, ephemeral: true as const } } : {}),
|
|
100
|
+
...(claudeCode ? { safety: { access: "read-only" as const, authentication: "existing-cli-required" as const, permissionMode: "plan" as const, tools: "none" as const, mcp: "empty-strict" as const, settingSources: "user" as const, userSettingsTrust: "required" as const, sessionPersistence: false as const } } : {}),
|
|
101
|
+
...(claudeCodeWriter ? { safety: { access: "workspace-write" as const, authentication: "existing-cli-required" as const, permissionMode: "acceptEdits" as const, tools: "Read,Write,Edit,Glob,Grep" as const, mcp: "empty-strict" as const, settingSources: "user" as const, userSettingsTrust: "required" as const, sessionPersistence: false as const } } : {}),
|
|
102
|
+
...(cursorAgent ? { safety: { access: "read-only" as const, authentication: "cursor-api-key-or-existing-login" as const, mode: "ask" as const, sandbox: "enabled" as const, workspaceTrust: "existing-required" as const, sessionReuse: false as const } } : {}),
|
|
103
|
+
...(cursorAgentWriter ? { safety: { access: "workspace-write" as const, authentication: "cursor-api-key-or-existing-login" as const, mode: "print" as const, sandbox: "enabled" as const, workspaceTrust: "existing-required" as const, sessionReuse: false as const } } : {}),
|
|
104
|
+
capabilities: {
|
|
105
|
+
stop: true,
|
|
106
|
+
steer: false,
|
|
107
|
+
resume: false,
|
|
108
|
+
structuredOutput: false,
|
|
109
|
+
toolEvents: false,
|
|
110
|
+
supervisor: "unsupported",
|
|
111
|
+
forkContext: false,
|
|
112
|
+
extensionBindings: false,
|
|
113
|
+
},
|
|
114
|
+
unsupportedReasons: unsupported,
|
|
115
|
+
nonResumableReason: unsupported.resume,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function normalizeExternalCliRunnerStatus(value: unknown): ExternalCliRunnerStatus | undefined {
|
|
120
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
121
|
+
const input = value as Record<string, unknown>;
|
|
122
|
+
if (input.type !== "external-cli" || typeof input.command !== "string" || !input.command.trim()) return undefined;
|
|
123
|
+
const args = Array.isArray(input.args) && input.args.every((arg) => typeof arg === "string")
|
|
124
|
+
? input.args
|
|
125
|
+
: undefined;
|
|
126
|
+
const promptDelivery = input.promptDelivery === "stdin" ? "stdin" : undefined;
|
|
127
|
+
const adapterId = input.adapter && typeof input.adapter === "object" && !Array.isArray(input.adapter)
|
|
128
|
+
? (input.adapter as Record<string, unknown>).id
|
|
129
|
+
: undefined;
|
|
130
|
+
if (adapterId === "grok-build") {
|
|
131
|
+
return {
|
|
132
|
+
type: "external-cli",
|
|
133
|
+
command: input.command,
|
|
134
|
+
args: args ?? [],
|
|
135
|
+
promptDelivery: "prompt-file",
|
|
136
|
+
adapter: { id: "grok-build", version: 1, executionMode: "one-shot-prompt-file" },
|
|
137
|
+
capabilities: { stop: true, steer: false, resume: false, structuredOutput: false, toolEvents: false, supervisor: "unsupported", forkContext: false, extensionBindings: false },
|
|
138
|
+
unsupportedReasons: PROMPT_FILE_UNSUPPORTED,
|
|
139
|
+
nonResumableReason: PROMPT_FILE_UNSUPPORTED.resume,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const adapter = isCodeOwnedExternalCliAdapterId(adapterId) ? adapterId : undefined;
|
|
143
|
+
return resolveExternalCliRunnerStatus({ ...(adapter ? { adapter } : {}), command: input.command, ...(args ? { args } : {}), ...(promptDelivery ? { promptDelivery } : {}) });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function externalCliReceiptMetadata(input: {
|
|
147
|
+
runner: ExternalCliRunnerStatus;
|
|
148
|
+
externalProcess?: ExternalProcessStatus;
|
|
149
|
+
outputReference?: string;
|
|
150
|
+
}): ExternalCliReceiptMetadata {
|
|
151
|
+
const { runner } = input;
|
|
152
|
+
return {
|
|
153
|
+
adapter: { ...runner.adapter },
|
|
154
|
+
capabilities: { ...runner.capabilities },
|
|
155
|
+
...(runner.safety ? { safety: { ...runner.safety } } : {}),
|
|
156
|
+
...(input.externalProcess ? {
|
|
157
|
+
outputArtifacts: {
|
|
158
|
+
stdoutPath: input.externalProcess.stdoutPath,
|
|
159
|
+
stderrPath: input.externalProcess.stderrPath,
|
|
160
|
+
...(input.outputReference || input.externalProcess.finalOutputPath ? { finalOutputPath: input.outputReference ?? input.externalProcess.finalOutputPath } : {}),
|
|
161
|
+
},
|
|
162
|
+
} : input.outputReference ? { outputArtifacts: { finalOutputPath: input.outputReference } } : {}),
|
|
163
|
+
handoff: { mode: "fresh" },
|
|
164
|
+
supervisor: { mode: "unsupported", reason: runner.unsupportedReasons.supervisor },
|
|
165
|
+
nonResumableReason: runner.nonResumableReason,
|
|
166
|
+
};
|
|
167
|
+
}
|