agentbox-sdk 0.1.503 → 0.1.511
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/README.md +141 -0
- package/dist/agents/index.d.ts +15 -4
- package/dist/agents/index.js +8 -4
- package/dist/{chunk-TUBBWIOM.js → chunk-76AFK7XN.js} +1 -1
- package/dist/{chunk-34LDITII.js → chunk-G3VEVR6Y.js} +1584 -530
- package/dist/{chunk-AVXJMCBC.js → chunk-MTQ2S46C.js} +22 -0
- package/dist/events/index.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -4
- package/dist/sandboxes/index.js +2 -2
- package/dist/{types-3VRJIiW0.d.ts → types-B4yy80AJ.d.ts} +86 -4
- package/package.json +5 -3
|
@@ -15,11 +15,12 @@ import {
|
|
|
15
15
|
debugRelay,
|
|
16
16
|
debugRuntime,
|
|
17
17
|
debugSetup,
|
|
18
|
+
getAvailablePort,
|
|
18
19
|
linesFromTextChunks,
|
|
19
20
|
sleep,
|
|
20
21
|
time,
|
|
21
22
|
waitFor
|
|
22
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-MTQ2S46C.js";
|
|
23
24
|
import {
|
|
24
25
|
shellQuote
|
|
25
26
|
} from "./chunk-NSJM57Z4.js";
|
|
@@ -29,16 +30,86 @@ import {
|
|
|
29
30
|
} from "./chunk-GOFJNFAD.js";
|
|
30
31
|
|
|
31
32
|
// src/agents/Agent.ts
|
|
32
|
-
import { randomUUID as
|
|
33
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
34
|
+
import path11 from "path";
|
|
33
35
|
|
|
34
36
|
// src/agents/providers/claude-code.ts
|
|
35
37
|
import { randomUUID } from "crypto";
|
|
38
|
+
|
|
39
|
+
// src/agents/transports/spawn.ts
|
|
40
|
+
import { spawn } from "child_process";
|
|
41
|
+
import { createInterface } from "readline";
|
|
42
|
+
function spawnCommand(options) {
|
|
43
|
+
const child = spawn(options.command, options.args ?? [], {
|
|
44
|
+
cwd: options.cwd,
|
|
45
|
+
env: options.env,
|
|
46
|
+
stdio: "pipe",
|
|
47
|
+
shell: process.platform === "win32",
|
|
48
|
+
windowsHide: true,
|
|
49
|
+
detached: options.processGroup === true && process.platform !== "win32"
|
|
50
|
+
});
|
|
51
|
+
const exitPromise = new Promise((resolve, reject) => {
|
|
52
|
+
child.once("error", reject);
|
|
53
|
+
child.once("close", (code) => resolve(code ?? 0));
|
|
54
|
+
});
|
|
55
|
+
void exitPromise.catch(() => void 0);
|
|
56
|
+
let killPromise;
|
|
57
|
+
const signalProcess = (signal) => {
|
|
58
|
+
try {
|
|
59
|
+
if (options.processGroup && process.platform !== "win32" && child.pid) process.kill(-child.pid, signal);
|
|
60
|
+
else child.kill(signal);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error.code !== "ESRCH") throw error;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const waitForExit = async (timeoutMs) => {
|
|
66
|
+
let timer;
|
|
67
|
+
try {
|
|
68
|
+
return await Promise.race([
|
|
69
|
+
exitPromise.then(() => true, () => true),
|
|
70
|
+
new Promise((resolve) => {
|
|
71
|
+
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
72
|
+
})
|
|
73
|
+
]);
|
|
74
|
+
} finally {
|
|
75
|
+
if (timer) clearTimeout(timer);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
child,
|
|
80
|
+
wait: () => exitPromise,
|
|
81
|
+
kill: (signal = "SIGTERM") => killPromise ??= (async () => {
|
|
82
|
+
signalProcess(signal);
|
|
83
|
+
if (await waitForExit(options.terminationTimeoutMs ?? 3e3)) {
|
|
84
|
+
if (options.processGroup && process.platform !== "win32") signalProcess("SIGKILL");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
signalProcess("SIGKILL");
|
|
88
|
+
if (!await waitForExit(3e3)) throw new Error("The owned agent process did not stop");
|
|
89
|
+
})()
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function* linesFromNodeStream(stream) {
|
|
93
|
+
const lineReader = createInterface({ input: stream });
|
|
94
|
+
try {
|
|
95
|
+
for await (const line of lineReader) {
|
|
96
|
+
yield line;
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
lineReader.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/agents/providers/claude-code.ts
|
|
36
104
|
import path8 from "path";
|
|
37
105
|
|
|
38
106
|
// src/agents/approval.ts
|
|
39
107
|
function getApprovalMode(options) {
|
|
40
108
|
return options.approvalMode ?? "auto";
|
|
41
109
|
}
|
|
110
|
+
function hasInteractiveQuestions(options) {
|
|
111
|
+
return options.interactiveQuestions ?? isInteractiveApproval(options);
|
|
112
|
+
}
|
|
42
113
|
function isInteractiveApproval(options) {
|
|
43
114
|
return getApprovalMode(options) === "interactive";
|
|
44
115
|
}
|
|
@@ -49,6 +120,58 @@ function shouldAutoApproveClaudeTools(options) {
|
|
|
49
120
|
return !isInteractiveApproval(options);
|
|
50
121
|
}
|
|
51
122
|
|
|
123
|
+
// src/agents/questions.ts
|
|
124
|
+
function record(value) {
|
|
125
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid agent question");
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
function text(value, max) {
|
|
129
|
+
if (typeof value !== "string" || !value.trim() || value.length > max) throw new Error("Invalid agent question text");
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
function normalizeUserQuestions(provider, input) {
|
|
133
|
+
const questions = record(input).questions;
|
|
134
|
+
if (!Array.isArray(questions) || !questions.length || questions.length > 10) throw new Error("Unsupported agent question count");
|
|
135
|
+
return questions.map((value, index) => {
|
|
136
|
+
const item = record(value);
|
|
137
|
+
if (item.isSecret === true) throw new Error("Secret questions cannot be answered through task history");
|
|
138
|
+
const options = item.options ?? [];
|
|
139
|
+
if (!Array.isArray(options) || options.length > 30) throw new Error("Unsupported agent question options");
|
|
140
|
+
const result = {
|
|
141
|
+
id: String(index),
|
|
142
|
+
question: text(item.question, 1e4),
|
|
143
|
+
...item.header ? { header: text(item.header, 200) } : {},
|
|
144
|
+
options: options.map((value2) => {
|
|
145
|
+
const option = record(value2);
|
|
146
|
+
return { label: text(option.label, 1e3), ...option.description ? { description: text(option.description, 1e4) } : {} };
|
|
147
|
+
}),
|
|
148
|
+
multiple: provider === "claude-code" ? item.multiSelect === true : provider === "open-code" && item.multiple === true,
|
|
149
|
+
allowCustom: provider === "open-code" ? item.custom !== false : true
|
|
150
|
+
};
|
|
151
|
+
if (new Set(result.options.map((option) => option.label)).size !== result.options.length) throw new Error("Duplicate agent question options");
|
|
152
|
+
if (!result.options.length && !result.allowCustom) throw new Error("Agent question has no available answers");
|
|
153
|
+
return result;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function validateUserAnswers(questions, answers) {
|
|
157
|
+
if (!questions?.length || !answers || answers.length !== questions.length || new Set(answers.map((answer) => answer.questionId)).size !== answers.length) throw new Error("Answer each agent question once");
|
|
158
|
+
return questions.map((question) => {
|
|
159
|
+
const answer = answers.find((answer2) => answer2.questionId === question.id);
|
|
160
|
+
if (!answer || !Array.isArray(answer.values) || !answer.values.length || answer.values.length > (question.multiple ? 30 : 1)) throw new Error("Choose an answer for each question");
|
|
161
|
+
if (new Set(answer.values).size !== answer.values.length || answer.values.some((value) => typeof value !== "string" || !value.trim() || value.length > 1e4 || !question.allowCustom && !question.options.some((option) => option.label === value))) throw new Error("Invalid answer to agent question");
|
|
162
|
+
return { questionId: question.id, values: [...answer.values] };
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function questionReply(provider, input, answers) {
|
|
166
|
+
const normalized = normalizeUserQuestions(provider, input);
|
|
167
|
+
const ordered = validateUserAnswers(normalized, answers);
|
|
168
|
+
if (provider === "open-code") return ordered.map((answer) => answer.values);
|
|
169
|
+
const original = record(input).questions;
|
|
170
|
+
const keys = original.map((item) => text(record(item)[provider === "codex" ? "id" : "question"], 1e4));
|
|
171
|
+
if (new Set(keys).size !== keys.length) throw new Error("Duplicate agent question IDs");
|
|
172
|
+
return Object.fromEntries(keys.map((key, index) => [key, provider === "codex" ? { answers: ordered[index].values } : ordered[index].values.join(", ")]));
|
|
173
|
+
}
|
|
174
|
+
|
|
52
175
|
// src/agents/input.ts
|
|
53
176
|
import { readFile } from "fs/promises";
|
|
54
177
|
import path from "path";
|
|
@@ -894,64 +1017,16 @@ function buildCodexConfigToml(opts = {}) {
|
|
|
894
1017
|
import { mkdir, chmod, rm, writeFile } from "fs/promises";
|
|
895
1018
|
import os from "os";
|
|
896
1019
|
import path4 from "path";
|
|
897
|
-
|
|
898
|
-
// src/agents/transports/spawn.ts
|
|
899
|
-
import { spawn } from "child_process";
|
|
900
|
-
import { createInterface } from "readline";
|
|
901
|
-
function spawnCommand(options) {
|
|
902
|
-
const child = spawn(options.command, options.args ?? [], {
|
|
903
|
-
cwd: options.cwd,
|
|
904
|
-
env: options.env,
|
|
905
|
-
stdio: "pipe",
|
|
906
|
-
shell: process.platform === "win32",
|
|
907
|
-
windowsHide: true
|
|
908
|
-
});
|
|
909
|
-
const exitPromise = new Promise((resolve, reject) => {
|
|
910
|
-
child.once("error", reject);
|
|
911
|
-
child.once("close", (code) => resolve(code ?? 0));
|
|
912
|
-
});
|
|
913
|
-
return {
|
|
914
|
-
child,
|
|
915
|
-
wait: () => exitPromise,
|
|
916
|
-
kill: async (signal = "SIGTERM") => {
|
|
917
|
-
child.kill(signal);
|
|
918
|
-
await exitPromise.catch(() => void 0);
|
|
919
|
-
}
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
async function waitForHttpReady(url, options) {
|
|
923
|
-
await waitFor(
|
|
924
|
-
async () => {
|
|
925
|
-
try {
|
|
926
|
-
const response = await fetch(url, options?.init);
|
|
927
|
-
return response.ok;
|
|
928
|
-
} catch {
|
|
929
|
-
return false;
|
|
930
|
-
}
|
|
931
|
-
},
|
|
932
|
-
{
|
|
933
|
-
timeoutMs: options?.timeoutMs,
|
|
934
|
-
intervalMs: options?.intervalMs
|
|
935
|
-
}
|
|
936
|
-
);
|
|
937
|
-
}
|
|
938
|
-
async function* linesFromNodeStream(stream) {
|
|
939
|
-
const lineReader = createInterface({ input: stream });
|
|
940
|
-
try {
|
|
941
|
-
for await (const line of lineReader) {
|
|
942
|
-
yield line;
|
|
943
|
-
}
|
|
944
|
-
} finally {
|
|
945
|
-
lineReader.close();
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// src/agents/config/setup.ts
|
|
950
1020
|
function shortLabel(command) {
|
|
951
1021
|
const oneLine = command.replace(/\s+/g, " ").trim();
|
|
952
1022
|
return oneLine.length > 60 ? `${oneLine.slice(0, 60)}\u2026` : oneLine;
|
|
953
1023
|
}
|
|
954
|
-
function agentboxRoot(provider, hasSandbox = true) {
|
|
1024
|
+
function agentboxRoot(provider, hasSandbox = true, stateDirectory) {
|
|
1025
|
+
if (stateDirectory !== void 0) {
|
|
1026
|
+
if (hasSandbox) throw new Error("stateDirectory is only supported for host execution.");
|
|
1027
|
+
if (!path4.isAbsolute(stateDirectory)) throw new Error("stateDirectory must be an absolute path.");
|
|
1028
|
+
return path4.join(stateDirectory, provider);
|
|
1029
|
+
}
|
|
955
1030
|
return hasSandbox ? `/tmp/agentbox/${provider}` : path4.join(os.tmpdir(), `agentbox-${provider}`);
|
|
956
1031
|
}
|
|
957
1032
|
function getAgentLayout(rootDir) {
|
|
@@ -1004,9 +1079,9 @@ var HostSetupTarget = class {
|
|
|
1004
1079
|
async () => {
|
|
1005
1080
|
await Promise.all(
|
|
1006
1081
|
files.map(async (entry) => {
|
|
1007
|
-
await mkdir(path4.dirname(entry.path), { recursive: true });
|
|
1082
|
+
await mkdir(path4.dirname(entry.path), { recursive: true, mode: 448 });
|
|
1008
1083
|
const content = typeof entry.content === "string" ? entry.content : entry.content;
|
|
1009
|
-
await writeFile(entry.path, content);
|
|
1084
|
+
await writeFile(entry.path, content, { mode: entry.mode ?? 384 });
|
|
1010
1085
|
if (entry.mode && (entry.mode & 73) !== 0) {
|
|
1011
1086
|
await chmod(entry.path, entry.mode);
|
|
1012
1087
|
}
|
|
@@ -1167,12 +1242,13 @@ async function createSetupTarget(provider, setupId, options) {
|
|
|
1167
1242
|
return time(debugRuntime, `createSetupTarget ${provider}`, async () => {
|
|
1168
1243
|
void setupId;
|
|
1169
1244
|
const layout = getAgentLayout(
|
|
1170
|
-
agentboxRoot(provider, Boolean(options.sandbox))
|
|
1245
|
+
agentboxRoot(provider, Boolean(options.sandbox), options.stateDirectory)
|
|
1171
1246
|
);
|
|
1172
1247
|
if (options.sandbox) {
|
|
1173
1248
|
return new SandboxSetupTarget(provider, layout, options);
|
|
1174
1249
|
}
|
|
1175
|
-
await mkdir(layout.homeDir, { recursive: true });
|
|
1250
|
+
await mkdir(layout.homeDir, { recursive: true, mode: 448 });
|
|
1251
|
+
await chmod(layout.homeDir, 448);
|
|
1176
1252
|
await mkdir(layout.xdgConfigHome, { recursive: true });
|
|
1177
1253
|
await mkdir(layout.agentsDir, { recursive: true });
|
|
1178
1254
|
await mkdir(layout.claudeDir, { recursive: true });
|
|
@@ -1749,8 +1825,203 @@ function extractOpenCodeCostData(events) {
|
|
|
1749
1825
|
}) : null;
|
|
1750
1826
|
}
|
|
1751
1827
|
|
|
1828
|
+
// src/agents/background-tasks.ts
|
|
1829
|
+
var DEFAULT_BACKGROUND_TASK_TIMEOUT_MS = 30 * 6e4;
|
|
1830
|
+
var BACKGROUND_TASK_GRACE_MS = 15e3;
|
|
1831
|
+
var CLI_BACKGROUND_WAIT_CEILING_ENV = "CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS";
|
|
1832
|
+
var CLI_BACKGROUND_WAIT_CEILING_MS = 3e4;
|
|
1833
|
+
function resolveBackgroundTaskTimeoutMs(value) {
|
|
1834
|
+
if (value === void 0) return DEFAULT_BACKGROUND_TASK_TIMEOUT_MS;
|
|
1835
|
+
if (Number.isNaN(value) || value < 0) {
|
|
1836
|
+
throw new Error(
|
|
1837
|
+
"backgroundTaskTimeoutMs must be a non-negative number (Infinity waits forever)."
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
return value;
|
|
1841
|
+
}
|
|
1842
|
+
function applyCliBackgroundWaitCeiling(env) {
|
|
1843
|
+
env[CLI_BACKGROUND_WAIT_CEILING_ENV] ??= String(
|
|
1844
|
+
CLI_BACKGROUND_WAIT_CEILING_MS
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
function asRecord2(value) {
|
|
1848
|
+
return value !== null && typeof value === "object" ? value : void 0;
|
|
1849
|
+
}
|
|
1850
|
+
function asArray(value) {
|
|
1851
|
+
return Array.isArray(value) ? value : [];
|
|
1852
|
+
}
|
|
1853
|
+
var SCHEDULE_TOOLS = /* @__PURE__ */ new Set(["CronCreate", "ScheduleWakeup"]);
|
|
1854
|
+
var DONE_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "killed"]);
|
|
1855
|
+
var BackgroundTaskTracker = class {
|
|
1856
|
+
tasks = /* @__PURE__ */ new Map();
|
|
1857
|
+
wakeups = /* @__PURE__ */ new Map();
|
|
1858
|
+
// tool_use seen, tool_result not yet: a failed schedule adds nothing.
|
|
1859
|
+
pendingWakeups = /* @__PURE__ */ new Map();
|
|
1860
|
+
afterResult = false;
|
|
1861
|
+
seenBackgroundWork = false;
|
|
1862
|
+
liveTasks() {
|
|
1863
|
+
return [...this.tasks.values(), ...this.wakeups.values()];
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* True once any background task or scheduled wakeup was live in this run.
|
|
1867
|
+
* The CLI queues a wake-up for every task that finishes and delivers it as
|
|
1868
|
+
* a new turn once the model is idle — including tasks that finished
|
|
1869
|
+
* mid-turn, whose queued turn starts right after that turn's `result`
|
|
1870
|
+
* with nothing live and nothing observable in between. After background
|
|
1871
|
+
* work has been seen, a `result` is therefore never the end of the run on
|
|
1872
|
+
* its own; only the grace passing without a new turn is.
|
|
1873
|
+
*/
|
|
1874
|
+
hasSeenBackgroundWork() {
|
|
1875
|
+
return this.seenBackgroundWork;
|
|
1876
|
+
}
|
|
1877
|
+
/** Feed one SDKMessage. Returns true when it started a follow-up turn. */
|
|
1878
|
+
ingest(message) {
|
|
1879
|
+
const m = asRecord2(message);
|
|
1880
|
+
if (!m) return false;
|
|
1881
|
+
if (m.type === "result") {
|
|
1882
|
+
this.afterResult = true;
|
|
1883
|
+
return false;
|
|
1884
|
+
}
|
|
1885
|
+
if (m.type === "system") return this.ingestSystem(m);
|
|
1886
|
+
if (m.type === "command_lifecycle") {
|
|
1887
|
+
if (m.state !== "started") return false;
|
|
1888
|
+
this.wakeups.clear();
|
|
1889
|
+
return this.startTurn();
|
|
1890
|
+
}
|
|
1891
|
+
if (m.parent_tool_use_id) return false;
|
|
1892
|
+
if (m.type === "assistant") {
|
|
1893
|
+
const started = this.startTurn();
|
|
1894
|
+
this.ingestToolUses(m);
|
|
1895
|
+
return started;
|
|
1896
|
+
}
|
|
1897
|
+
if (m.type === "user") {
|
|
1898
|
+
this.ingestToolResults(m);
|
|
1899
|
+
return false;
|
|
1900
|
+
}
|
|
1901
|
+
if (m.type === "stream_event")
|
|
1902
|
+
return asRecord2(m.event)?.type === "message_start" && this.startTurn();
|
|
1903
|
+
return false;
|
|
1904
|
+
}
|
|
1905
|
+
startTurn() {
|
|
1906
|
+
if (!this.afterResult) return false;
|
|
1907
|
+
this.afterResult = false;
|
|
1908
|
+
return true;
|
|
1909
|
+
}
|
|
1910
|
+
ingestSystem(m) {
|
|
1911
|
+
const id = String(m.task_id ?? "");
|
|
1912
|
+
switch (m.subtype) {
|
|
1913
|
+
case "background_tasks_changed":
|
|
1914
|
+
this.tasks.clear();
|
|
1915
|
+
for (const entry of asArray(m.tasks)) {
|
|
1916
|
+
const task = asRecord2(entry);
|
|
1917
|
+
if (task) this.addTask(task);
|
|
1918
|
+
}
|
|
1919
|
+
return false;
|
|
1920
|
+
case "task_started":
|
|
1921
|
+
if (m.is_backgrounded === true && !m.owned_by_subagent) this.addTask(m);
|
|
1922
|
+
return false;
|
|
1923
|
+
case "task_notification":
|
|
1924
|
+
this.tasks.delete(id);
|
|
1925
|
+
return false;
|
|
1926
|
+
case "task_updated":
|
|
1927
|
+
if (DONE_STATUSES.has(String(asRecord2(m.patch)?.status)))
|
|
1928
|
+
this.tasks.delete(id);
|
|
1929
|
+
return false;
|
|
1930
|
+
case "init":
|
|
1931
|
+
return this.startTurn();
|
|
1932
|
+
default:
|
|
1933
|
+
return false;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
addTask(task) {
|
|
1937
|
+
const id = String(task.task_id ?? "");
|
|
1938
|
+
if (!id) return;
|
|
1939
|
+
this.seenBackgroundWork = true;
|
|
1940
|
+
this.tasks.set(id, {
|
|
1941
|
+
id,
|
|
1942
|
+
type: String(task.task_type ?? "task"),
|
|
1943
|
+
description: String(task.description ?? "")
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
ingestToolUses(m) {
|
|
1947
|
+
for (const entry of asArray(asRecord2(m.message)?.content)) {
|
|
1948
|
+
const block = asRecord2(entry);
|
|
1949
|
+
if (block?.type !== "tool_use") continue;
|
|
1950
|
+
const name = String(block.name ?? "");
|
|
1951
|
+
const input = asRecord2(block.input) ?? {};
|
|
1952
|
+
if (name === "CronDelete" || name === "ScheduleWakeup" && input.stop === true) {
|
|
1953
|
+
this.wakeups.clear();
|
|
1954
|
+
continue;
|
|
1955
|
+
}
|
|
1956
|
+
if (!SCHEDULE_TOOLS.has(name)) continue;
|
|
1957
|
+
const id = String(block.id ?? "");
|
|
1958
|
+
if (!id) continue;
|
|
1959
|
+
this.pendingWakeups.set(id, {
|
|
1960
|
+
id,
|
|
1961
|
+
type: "scheduled_wakeup",
|
|
1962
|
+
description: String(input.prompt ?? input.cron ?? name)
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
ingestToolResults(m) {
|
|
1967
|
+
for (const entry of asArray(asRecord2(m.message)?.content)) {
|
|
1968
|
+
const block = asRecord2(entry);
|
|
1969
|
+
if (block?.type !== "tool_result") continue;
|
|
1970
|
+
const id = String(block.tool_use_id ?? "");
|
|
1971
|
+
const pending = this.pendingWakeups.get(id);
|
|
1972
|
+
if (!pending) continue;
|
|
1973
|
+
this.pendingWakeups.delete(id);
|
|
1974
|
+
if (block.is_error) continue;
|
|
1975
|
+
this.seenBackgroundWork = true;
|
|
1976
|
+
this.wakeups.set(id, pending);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
var MAX_TIMER_MS = 2 ** 31 - 1;
|
|
1981
|
+
var STOP_TASKS_TIMEOUT_MS = 5e3;
|
|
1982
|
+
function withTimeout(promise, ms) {
|
|
1983
|
+
let timer;
|
|
1984
|
+
const timeout = new Promise((resolve) => {
|
|
1985
|
+
timer = setTimeout(() => resolve(void 0), ms);
|
|
1986
|
+
});
|
|
1987
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
1988
|
+
}
|
|
1989
|
+
var BackgroundWait = class {
|
|
1990
|
+
constructor(graceMs, ceilingMs) {
|
|
1991
|
+
this.graceMs = graceMs;
|
|
1992
|
+
this.expired = new Promise((resolve) => {
|
|
1993
|
+
this.expire = resolve;
|
|
1994
|
+
});
|
|
1995
|
+
if (Number.isFinite(ceilingMs)) {
|
|
1996
|
+
this.ceiling = setTimeout(() => this.expire("ceiling"), Math.min(ceilingMs, MAX_TIMER_MS));
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
graceMs;
|
|
2000
|
+
expired;
|
|
2001
|
+
expire;
|
|
2002
|
+
grace;
|
|
2003
|
+
ceiling;
|
|
2004
|
+
startedAt = Date.now();
|
|
2005
|
+
/** Arm the grace timer while nothing is live; disarm it once a task appears. */
|
|
2006
|
+
setIdle(idle) {
|
|
2007
|
+
if (!idle) {
|
|
2008
|
+
clearTimeout(this.grace);
|
|
2009
|
+
this.grace = void 0;
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
this.grace ??= setTimeout(() => this.expire("grace"), this.graceMs);
|
|
2013
|
+
}
|
|
2014
|
+
elapsedMs() {
|
|
2015
|
+
return Date.now() - this.startedAt;
|
|
2016
|
+
}
|
|
2017
|
+
clear() {
|
|
2018
|
+
clearTimeout(this.grace);
|
|
2019
|
+
clearTimeout(this.ceiling);
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
|
|
1752
2023
|
// src/agents/providers/claude-code.ts
|
|
1753
|
-
var DAEMON_PROTOCOL_VERSION = "
|
|
2024
|
+
var DAEMON_PROTOCOL_VERSION = "5";
|
|
1754
2025
|
var DAEMON_PORT = 43180;
|
|
1755
2026
|
var DAEMON_PATH = "/tmp/agentbox/claude-code/daemon.mjs";
|
|
1756
2027
|
var DAEMON_LOG_PATH = "/tmp/agentbox/claude-code/daemon.log";
|
|
@@ -1760,7 +2031,7 @@ var DAEMON_READY_TIMEOUT_MS = 3e4;
|
|
|
1760
2031
|
var DAEMON_READY_POLL_INTERVAL_MS = 250;
|
|
1761
2032
|
function claudeConfigDir(options) {
|
|
1762
2033
|
return path8.join(
|
|
1763
|
-
agentboxRoot(AgentProvider.ClaudeCode, Boolean(options.sandbox)),
|
|
2034
|
+
agentboxRoot(AgentProvider.ClaudeCode, Boolean(options.sandbox), options.stateDirectory),
|
|
1764
2035
|
".claude"
|
|
1765
2036
|
);
|
|
1766
2037
|
}
|
|
@@ -1768,7 +2039,7 @@ function buildClaudeQueryOptions(params) {
|
|
|
1768
2039
|
const provider = params.request.options.provider;
|
|
1769
2040
|
const run = params.request.run;
|
|
1770
2041
|
const extraArgs = {
|
|
1771
|
-
"mcp-config": params.mcpConfigPath
|
|
2042
|
+
...params.mcpConfigPath ? { "mcp-config": params.mcpConfigPath } : {}
|
|
1772
2043
|
};
|
|
1773
2044
|
for (const arg of provider?.args ?? []) {
|
|
1774
2045
|
if (typeof arg !== "string") continue;
|
|
@@ -1785,7 +2056,11 @@ function buildClaudeQueryOptions(params) {
|
|
|
1785
2056
|
cwd: params.cwd ?? params.request.options.cwd,
|
|
1786
2057
|
env: params.env,
|
|
1787
2058
|
pathToClaudeCodeExecutable: provider?.binary ?? "claude",
|
|
1788
|
-
settings: params.settingsPath,
|
|
2059
|
+
...params.settingsPath ? { settings: params.settingsPath } : {},
|
|
2060
|
+
...params.request.options.configuration === "native" ? {
|
|
2061
|
+
settingSources: ["user", "project", "local"],
|
|
2062
|
+
systemPrompt: { type: "preset", preset: "claude_code" }
|
|
2063
|
+
} : {},
|
|
1789
2064
|
extraArgs,
|
|
1790
2065
|
includePartialMessages: true,
|
|
1791
2066
|
forwardSubagentText: true,
|
|
@@ -1794,8 +2069,8 @@ function buildClaudeQueryOptions(params) {
|
|
|
1794
2069
|
...provider?.additionalDirectories?.length ? { additionalDirectories: provider.additionalDirectories } : {},
|
|
1795
2070
|
...run.model ? { model: run.model } : {},
|
|
1796
2071
|
...effort ? { effort } : {},
|
|
1797
|
-
...provider?.permissionMode ? { permissionMode: provider.permissionMode } : {},
|
|
1798
|
-
...provider?.permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {},
|
|
2072
|
+
...run.mode === "plan" ? { permissionMode: "plan" } : params.request.options.fullAccess ? { permissionMode: "bypassPermissions" } : run.mode === "default" ? { permissionMode: "default" } : provider?.permissionMode ? { permissionMode: provider.permissionMode } : {},
|
|
2073
|
+
...params.request.options.fullAccess || provider?.permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {},
|
|
1799
2074
|
...provider?.allowedTools?.length ? { allowedTools: provider.allowedTools } : {},
|
|
1800
2075
|
...run.resumeSessionId ? { resume: run.resumeSessionId } : {},
|
|
1801
2076
|
// Fork-at-message: claude-agent-sdk natively supports slicing a
|
|
@@ -1941,8 +2216,14 @@ function readJsonBody(req) {
|
|
|
1941
2216
|
});
|
|
1942
2217
|
}
|
|
1943
2218
|
|
|
1944
|
-
function
|
|
1945
|
-
|
|
2219
|
+
async function handlePermission(req, res, runId) {
|
|
2220
|
+
const run = liveRuns.get(runId);
|
|
2221
|
+
const body = await readJsonBody(req);
|
|
2222
|
+
const resolve = run?.permissions.get(body.requestId);
|
|
2223
|
+
if (!resolve) { res.writeHead(409); res.end("Request is no longer pending"); return; }
|
|
2224
|
+
run.permissions.delete(body.requestId);
|
|
2225
|
+
resolve(body.response);
|
|
2226
|
+
res.writeHead(204); res.end();
|
|
1946
2227
|
}
|
|
1947
2228
|
|
|
1948
2229
|
async function handleStart(req, res, runId) {
|
|
@@ -1982,10 +2263,52 @@ async function handleStart(req, res, runId) {
|
|
|
1982
2263
|
const opts = { ...(options || {}) };
|
|
1983
2264
|
const autoApprove = !!opts.autoApproveTools;
|
|
1984
2265
|
delete opts.autoApproveTools;
|
|
2266
|
+
const interactiveQuestions = !!opts.interactiveQuestions;
|
|
2267
|
+
delete opts.interactiveQuestions;
|
|
2268
|
+
let planning = opts.permissionMode === "plan";
|
|
2269
|
+
const permissions = new Map();
|
|
2270
|
+
const clearPermissions = () => { for (const resolve of permissions.values()) resolve({ behavior: "deny", message: "Run ended", interrupt: true }); permissions.clear(); };
|
|
2271
|
+
const canUseTool = async (toolName, input, context) => {
|
|
2272
|
+
const isQuestion = toolName === "AskUserQuestion";
|
|
2273
|
+
const isPlan = toolName === "ExitPlanMode";
|
|
2274
|
+
if (context.signal.aborted) return { behavior: "deny", message: "Run cancelled", interrupt: true };
|
|
2275
|
+
if ((isQuestion || isPlan) && !interactiveQuestions) return { behavior: "deny", message: "No interactive user is available." };
|
|
2276
|
+
if (!isQuestion && !isPlan && planning && toolName !== "EnterPlanMode") return { behavior: "deny", message: "Finish planning before requesting write access." };
|
|
2277
|
+
if (!isQuestion && !isPlan && autoApprove) return { behavior: "allow", updatedInput: input };
|
|
2278
|
+
return new Promise((resolve) => {
|
|
2279
|
+
const abort = () => { permissions.delete(context.toolUseID); resolve({ behavior: "deny", message: "Run cancelled", interrupt: true }); };
|
|
2280
|
+
permissions.set(context.toolUseID, (response) => { context.signal.removeEventListener("abort", abort); if (isPlan && response.behavior === "allow") planning = false; resolve(response); });
|
|
2281
|
+
context.signal.addEventListener("abort", abort, { once: true });
|
|
2282
|
+
res.write(JSON.stringify({ _permission: { requestId: context.toolUseID, toolName, input, title: context.title } }) + "\\n");
|
|
2283
|
+
});
|
|
2284
|
+
};
|
|
1985
2285
|
opts.pathToClaudeCodeExecutable = resolveClaudeBinary(
|
|
1986
2286
|
opts.pathToClaudeCodeExecutable,
|
|
1987
2287
|
);
|
|
1988
2288
|
|
|
2289
|
+
let queryHandle;
|
|
2290
|
+
let clientGone = false;
|
|
2291
|
+
// This run's own teardown. The liveRuns entry is only removed when it is
|
|
2292
|
+
// still ours: hosts reuse a runId across retry attempts, and a successor
|
|
2293
|
+
// registered while our CLI winds down must not be evicted by our exit.
|
|
2294
|
+
const releaseRun = () => {
|
|
2295
|
+
clearInterval(heartbeat);
|
|
2296
|
+
clearPermissions();
|
|
2297
|
+
if (liveRuns.get(runId)?.query === queryHandle) liveRuns.delete(runId);
|
|
2298
|
+
promptStream.end();
|
|
2299
|
+
};
|
|
2300
|
+
// Host gone (settled, cancelled or crashed) \u2192 end the prompt so the CLI
|
|
2301
|
+
// winds down instead of living on with its background work. Detected on
|
|
2302
|
+
// the response: \`req\` emits "close" as soon as its body is consumed (Node
|
|
2303
|
+
// >= 16), long before any disconnect, while the response only closes
|
|
2304
|
+
// early when the socket dies before the stream finished.
|
|
2305
|
+
res.on("close", () => {
|
|
2306
|
+
if (res.writableFinished) return;
|
|
2307
|
+
clientGone = true;
|
|
2308
|
+
releaseRun();
|
|
2309
|
+
queryHandle?.interrupt().catch(() => {});
|
|
2310
|
+
});
|
|
2311
|
+
|
|
1989
2312
|
// Resume-if-exists gate. \`claude --resume <id>\` errors hard with "No
|
|
1990
2313
|
// conversation found with session ID" when the local session jsonl is
|
|
1991
2314
|
// missing \u2014 most often because a prior post-task snapshot failed and the
|
|
@@ -2016,13 +2339,19 @@ async function handleStart(req, res, runId) {
|
|
|
2016
2339
|
}
|
|
2017
2340
|
}
|
|
2018
2341
|
|
|
2019
|
-
|
|
2342
|
+
// Nobody left to stream to: do not start a CLI for it.
|
|
2343
|
+
if (clientGone) { res.end(); return; }
|
|
2020
2344
|
try {
|
|
2021
2345
|
queryHandle = query({
|
|
2022
2346
|
prompt: promptStream,
|
|
2023
2347
|
options: {
|
|
2024
2348
|
...opts,
|
|
2025
|
-
|
|
2349
|
+
canUseTool,
|
|
2350
|
+
hooks: { PreToolUse: [{ hooks: [async (input) => {
|
|
2351
|
+
planning = input.permission_mode === "plan";
|
|
2352
|
+
return interactiveQuestions && ["AskUserQuestion", "ExitPlanMode"].includes(input.tool_name)
|
|
2353
|
+
? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask" } } : {};
|
|
2354
|
+
}] }] },
|
|
2026
2355
|
},
|
|
2027
2356
|
});
|
|
2028
2357
|
} catch (e) {
|
|
@@ -2032,28 +2361,21 @@ async function handleStart(req, res, runId) {
|
|
|
2032
2361
|
return;
|
|
2033
2362
|
}
|
|
2034
2363
|
|
|
2035
|
-
liveRuns.set(runId, { query: queryHandle, prompt: promptStream });
|
|
2036
|
-
|
|
2037
|
-
// Client disconnected (e.g. host process killed) \u2192 tear down.
|
|
2038
|
-
req.on("close", () => {
|
|
2039
|
-
clearInterval(heartbeat);
|
|
2040
|
-
if (!liveRuns.has(runId)) return;
|
|
2041
|
-
liveRuns.delete(runId);
|
|
2042
|
-
promptStream.end();
|
|
2043
|
-
queryHandle.interrupt().catch(() => {});
|
|
2044
|
-
});
|
|
2364
|
+
liveRuns.set(runId, { query: queryHandle, prompt: promptStream, permissions });
|
|
2045
2365
|
|
|
2366
|
+
// Forward every SDKMessage, not just up to the first result: in
|
|
2367
|
+
// streaming-input mode the CLI keeps running after a turn ends and
|
|
2368
|
+
// re-prompts the model when background work finishes. The host decides
|
|
2369
|
+
// when the run is over and disconnects (res "close" above), which ends
|
|
2370
|
+
// the prompt and lets the CLI wind down.
|
|
2046
2371
|
try {
|
|
2047
2372
|
for await (const message of queryHandle) {
|
|
2048
2373
|
res.write(JSON.stringify(message) + "\\n");
|
|
2049
|
-
if (message.type === "result") break;
|
|
2050
2374
|
}
|
|
2051
2375
|
} catch (e) {
|
|
2052
2376
|
res.write(JSON.stringify({ _error: String(e?.message ?? e) }) + "\\n");
|
|
2053
2377
|
} finally {
|
|
2054
|
-
|
|
2055
|
-
liveRuns.delete(runId);
|
|
2056
|
-
promptStream.end();
|
|
2378
|
+
releaseRun();
|
|
2057
2379
|
res.end();
|
|
2058
2380
|
}
|
|
2059
2381
|
}
|
|
@@ -2119,6 +2441,11 @@ const server = http.createServer((req, res) => {
|
|
|
2119
2441
|
return;
|
|
2120
2442
|
}
|
|
2121
2443
|
const url = req.url ?? "";
|
|
2444
|
+
const permissionRoute = url.match(/^\\/runs\\/([^/]+)\\/permission$/);
|
|
2445
|
+
if (req.method === "POST" && permissionRoute) {
|
|
2446
|
+
handlePermission(req, res, decodeURIComponent(permissionRoute[1])).catch(() => { if (!res.headersSent) res.writeHead(400); res.end(); });
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2122
2449
|
let m;
|
|
2123
2450
|
if (req.method === "POST" && (m = url.match(/^\\/runs\\/([^/]+)\\/start$/))) {
|
|
2124
2451
|
handleStart(req, res, decodeURIComponent(m[1]));
|
|
@@ -2355,15 +2682,11 @@ var ClaudeCodeAgentAdapter = class {
|
|
|
2355
2682
|
).catch(() => void 0);
|
|
2356
2683
|
}
|
|
2357
2684
|
async setup(request) {
|
|
2685
|
+
if (request.options.configuration === "native") return;
|
|
2358
2686
|
await time(debugClaude, "claude-code setup()", async () => {
|
|
2359
2687
|
const options = request.options;
|
|
2360
2688
|
const provider = request.provider;
|
|
2361
2689
|
const sandbox = options.sandbox;
|
|
2362
|
-
if (!sandbox) {
|
|
2363
|
-
throw new Error(
|
|
2364
|
-
"claude-code requires a sandbox (the SDK transport runs as a daemon inside the sandbox)."
|
|
2365
|
-
);
|
|
2366
|
-
}
|
|
2367
2690
|
const target = await createSetupTarget(provider, "shared-setup", options);
|
|
2368
2691
|
const settingsPath = path8.join(target.layout.claudeDir, "settings.json");
|
|
2369
2692
|
const mcpConfigPath = path8.join(
|
|
@@ -2384,6 +2707,7 @@ var ClaudeCodeAgentAdapter = class {
|
|
|
2384
2707
|
const claudeSettings = { ...hookSettings, ...workflowSettings };
|
|
2385
2708
|
const mcpConfigJson = buildClaudeMcpConfig(options.mcps) ?? JSON.stringify({ mcpServers: {} }, null, 2);
|
|
2386
2709
|
const artifacts = [
|
|
2710
|
+
...!sandbox ? [{ path: path8.join(target.layout.claudeDir, ".claude-plugin", "plugin.json"), content: JSON.stringify({ name: "agentbox", version: "1.0.0" }) }] : [],
|
|
2387
2711
|
...skillArtifacts,
|
|
2388
2712
|
...buildClaudeCommandArtifacts(options.commands, target.layout),
|
|
2389
2713
|
...buildClaudeSubagentArtifacts(options.subAgents, target.layout),
|
|
@@ -2394,11 +2718,11 @@ var ClaudeCodeAgentAdapter = class {
|
|
|
2394
2718
|
{ path: mcpConfigPath, content: mcpConfigJson }
|
|
2395
2719
|
];
|
|
2396
2720
|
const enableRtk = options.enableRtk === true;
|
|
2397
|
-
const daemonInfo = {
|
|
2721
|
+
const daemonInfo = sandbox ? {
|
|
2398
2722
|
port: DAEMON_PORT,
|
|
2399
2723
|
healthPath: "/__version",
|
|
2400
2724
|
expectedVersionMatch: DAEMON_PROTOCOL_VERSION
|
|
2401
|
-
};
|
|
2725
|
+
} : void 0;
|
|
2402
2726
|
const setupId = computeSetupId({
|
|
2403
2727
|
artifacts,
|
|
2404
2728
|
installCommands,
|
|
@@ -2416,7 +2740,7 @@ var ClaudeCodeAgentAdapter = class {
|
|
|
2416
2740
|
"applyDifferentialSetup",
|
|
2417
2741
|
() => applyDifferentialSetup(target, artifacts, installCommands)
|
|
2418
2742
|
),
|
|
2419
|
-
ensureClaudeCodeDaemon(options, env)
|
|
2743
|
+
...sandbox ? [ensureClaudeCodeDaemon(options, env)] : []
|
|
2420
2744
|
]);
|
|
2421
2745
|
if (enableRtk) {
|
|
2422
2746
|
await time(debugClaude, "activateRtk", () => activateRtk(target));
|
|
@@ -2428,11 +2752,7 @@ var ClaudeCodeAgentAdapter = class {
|
|
|
2428
2752
|
const executeStartedAt = Date.now();
|
|
2429
2753
|
debugClaude("execute() start runId=%s", request.runId);
|
|
2430
2754
|
const sandbox = request.options.sandbox;
|
|
2431
|
-
if (!sandbox)
|
|
2432
|
-
throw new Error(
|
|
2433
|
-
"claude-code requires a sandbox (the SDK transport runs as a daemon inside the sandbox)."
|
|
2434
|
-
);
|
|
2435
|
-
}
|
|
2755
|
+
if (!sandbox) return executeNativeClaude(request, sink);
|
|
2436
2756
|
const claudeDir = claudeConfigDir(request.options);
|
|
2437
2757
|
const settingsPath = path8.join(claudeDir, "settings.json");
|
|
2438
2758
|
const mcpConfigPath = path8.join(claudeDir, "agentbox-mcp.json");
|
|
@@ -2445,6 +2765,7 @@ var ClaudeCodeAgentAdapter = class {
|
|
|
2445
2765
|
// user inside our images.
|
|
2446
2766
|
IS_SANDBOX: "1"
|
|
2447
2767
|
};
|
|
2768
|
+
applyCliBackgroundWaitCeiling(env);
|
|
2448
2769
|
const customHeaders = request.options.customHeaders;
|
|
2449
2770
|
if (customHeaders && Object.keys(customHeaders).length > 0) {
|
|
2450
2771
|
const serialized = Object.entries(customHeaders).map(([name, value]) => `${name}: ${value}`).join("\n");
|
|
@@ -2489,21 +2810,28 @@ ${serialized}` : serialized;
|
|
|
2489
2810
|
// already set `resume` for the resume path, so only stamp `sessionId` for
|
|
2490
2811
|
// fresh runs.
|
|
2491
2812
|
...request.run.resumeSessionId ? {} : { sessionId: presetSessionId },
|
|
2492
|
-
autoApproveTools
|
|
2813
|
+
autoApproveTools,
|
|
2814
|
+
interactiveQuestions: hasInteractiveQuestions(request.options)
|
|
2493
2815
|
}
|
|
2494
2816
|
};
|
|
2495
2817
|
const fetchAbort = new AbortController();
|
|
2818
|
+
const runUrl = `${baseUrl}/runs/${encodeURIComponent(request.runId)}`;
|
|
2496
2819
|
const cleanup = async () => {
|
|
2497
2820
|
try {
|
|
2498
|
-
await fetch(
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2821
|
+
await fetch(`${runUrl}/abort`, { method: "POST", headers: authHeaders });
|
|
2822
|
+
} catch {
|
|
2823
|
+
}
|
|
2824
|
+
try {
|
|
2825
|
+
await fetch(runUrl, { method: "DELETE", headers: authHeaders, signal: AbortSignal.timeout(3e3) });
|
|
2502
2826
|
} catch {
|
|
2503
2827
|
}
|
|
2504
2828
|
fetchAbort.abort();
|
|
2505
2829
|
};
|
|
2506
|
-
|
|
2830
|
+
let cancelled = false;
|
|
2831
|
+
sink.setAbort(async () => {
|
|
2832
|
+
cancelled = true;
|
|
2833
|
+
await cleanup();
|
|
2834
|
+
});
|
|
2507
2835
|
sink.onMessage(async (content) => {
|
|
2508
2836
|
const parts = await validateProviderUserInput(request.provider, content);
|
|
2509
2837
|
const mapped = mapToClaudeUserContent(parts);
|
|
@@ -2535,9 +2863,9 @@ ${serialized}` : serialized;
|
|
|
2535
2863
|
})
|
|
2536
2864
|
);
|
|
2537
2865
|
if (!response.ok || !response.body) {
|
|
2538
|
-
const
|
|
2866
|
+
const text2 = await response.text().catch(() => "");
|
|
2539
2867
|
throw new Error(
|
|
2540
|
-
`claude-code daemon /start failed: ${response.status} ${
|
|
2868
|
+
`claude-code daemon /start failed: ${response.status} ${text2}`
|
|
2541
2869
|
);
|
|
2542
2870
|
}
|
|
2543
2871
|
sink.setRaw({ baseUrl, runId: request.runId, claudeDir });
|
|
@@ -2554,199 +2882,22 @@ ${serialized}` : serialized;
|
|
|
2554
2882
|
{ messageId: initialUuid }
|
|
2555
2883
|
)
|
|
2556
2884
|
);
|
|
2557
|
-
|
|
2558
|
-
let streamedThinkingChars = 0;
|
|
2559
|
-
let pendingMessages = 1;
|
|
2560
|
-
let firstStreamEventLogged = false;
|
|
2561
|
-
let firstTextDeltaLogged = false;
|
|
2562
|
-
let lastTerminalReason;
|
|
2563
|
-
let lastIsError = false;
|
|
2564
|
-
const rawPayloads = [];
|
|
2565
|
-
try {
|
|
2885
|
+
const permissionMessages = async function* () {
|
|
2566
2886
|
for await (const item of parseNdjsonStream(response.body)) {
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
throw new Error(
|
|
2571
|
-
String(item._error ?? "daemon error")
|
|
2572
|
-
);
|
|
2573
|
-
}
|
|
2574
|
-
if ("_notice" in ctrl) {
|
|
2575
|
-
debugClaude("daemon notice: %o", ctrl);
|
|
2576
|
-
sink.emitRaw(
|
|
2577
|
-
toRawEvent(
|
|
2578
|
-
request.runId,
|
|
2579
|
-
ctrl,
|
|
2580
|
-
`daemon.${String(ctrl._notice ?? "notice")}`
|
|
2581
|
-
)
|
|
2582
|
-
);
|
|
2583
|
-
continue;
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
2586
|
-
const message = item;
|
|
2587
|
-
rawPayloads.push(message);
|
|
2588
|
-
sink.emitRaw(toRawEvent(request.runId, message, message.type));
|
|
2589
|
-
if (message.type === "system") {
|
|
2590
|
-
const sub = message.subtype;
|
|
2591
|
-
if (sub === "init") {
|
|
2592
|
-
const sys = message;
|
|
2593
|
-
if (sys.session_id) {
|
|
2594
|
-
debugClaude(
|
|
2595
|
-
"\u2605 session.init session_id=%s (%dms)",
|
|
2596
|
-
sys.session_id.slice(0, 8),
|
|
2597
|
-
Date.now() - executeStartedAt
|
|
2598
|
-
);
|
|
2599
|
-
}
|
|
2600
|
-
} else if (sub === "hook_started") {
|
|
2601
|
-
const h = message;
|
|
2602
|
-
debugClaude(
|
|
2603
|
-
"hook.started name=%s event=%s hook_id=%s",
|
|
2604
|
-
h.hook_name,
|
|
2605
|
-
h.hook_event,
|
|
2606
|
-
h.hook_id
|
|
2607
|
-
);
|
|
2608
|
-
} else if (sub === "hook_response") {
|
|
2609
|
-
const h = message;
|
|
2610
|
-
const stderr = h.stderr && h.stderr.length > 0 ? h.stderr.replace(/\s+$/, "") : void 0;
|
|
2611
|
-
debugClaude(
|
|
2612
|
-
"hook.response name=%s exit=%s outcome=%s%s",
|
|
2613
|
-
h.hook_name,
|
|
2614
|
-
h.exit_code,
|
|
2615
|
-
h.outcome,
|
|
2616
|
-
stderr ? ` stderr=${JSON.stringify(stderr).slice(0, 200)}` : ""
|
|
2617
|
-
);
|
|
2618
|
-
}
|
|
2619
|
-
continue;
|
|
2620
|
-
}
|
|
2621
|
-
if (message.type === "stream_event") {
|
|
2622
|
-
if (!firstStreamEventLogged) {
|
|
2623
|
-
firstStreamEventLogged = true;
|
|
2624
|
-
debugClaude(
|
|
2625
|
-
"\u2605 first stream_event (%dms since execute start)",
|
|
2626
|
-
Date.now() - executeStartedAt
|
|
2627
|
-
);
|
|
2628
|
-
}
|
|
2629
|
-
const partial = message;
|
|
2630
|
-
if (partial.parent_tool_use_id) continue;
|
|
2631
|
-
const streamType = partial.event?.type;
|
|
2632
|
-
if (streamType === "message_start") {
|
|
2633
|
-
accumulatedText = "";
|
|
2634
|
-
streamedThinkingChars = 0;
|
|
2635
|
-
}
|
|
2636
|
-
const { text, thinking } = extractStreamDeltas(partial);
|
|
2637
|
-
if (thinking) {
|
|
2638
|
-
streamedThinkingChars += thinking.length;
|
|
2639
|
-
sink.emitEvent(
|
|
2640
|
-
createNormalizedEvent(
|
|
2641
|
-
"reasoning.delta",
|
|
2642
|
-
{ provider: request.provider, runId: request.runId },
|
|
2643
|
-
{ delta: thinking }
|
|
2644
|
-
)
|
|
2645
|
-
);
|
|
2646
|
-
}
|
|
2647
|
-
if (text) {
|
|
2648
|
-
if (!firstTextDeltaLogged) {
|
|
2649
|
-
firstTextDeltaLogged = true;
|
|
2650
|
-
debugClaude(
|
|
2651
|
-
"\u2605 first text delta (%dms since execute start)",
|
|
2652
|
-
Date.now() - executeStartedAt
|
|
2653
|
-
);
|
|
2654
|
-
}
|
|
2655
|
-
accumulatedText += text;
|
|
2656
|
-
sink.emitEvent(
|
|
2657
|
-
createNormalizedEvent(
|
|
2658
|
-
"text.delta",
|
|
2659
|
-
{ provider: request.provider, runId: request.runId },
|
|
2660
|
-
{ delta: text }
|
|
2661
|
-
)
|
|
2662
|
-
);
|
|
2663
|
-
}
|
|
2664
|
-
continue;
|
|
2665
|
-
}
|
|
2666
|
-
if (message.type === "assistant") {
|
|
2667
|
-
const asst = message;
|
|
2668
|
-
if (asst.parent_tool_use_id) continue;
|
|
2669
|
-
const thinking = extractAssistantThinking(asst);
|
|
2670
|
-
if (thinking && streamedThinkingChars === 0) {
|
|
2671
|
-
sink.emitEvent(
|
|
2672
|
-
createNormalizedEvent(
|
|
2673
|
-
"reasoning.delta",
|
|
2674
|
-
{ provider: request.provider, runId: request.runId },
|
|
2675
|
-
{ delta: thinking }
|
|
2676
|
-
)
|
|
2677
|
-
);
|
|
2678
|
-
}
|
|
2679
|
-
const text = extractAssistantText(asst);
|
|
2680
|
-
sink.emitEvent(
|
|
2681
|
-
createNormalizedEvent(
|
|
2682
|
-
"message.completed",
|
|
2683
|
-
{ provider: request.provider, runId: request.runId },
|
|
2684
|
-
{
|
|
2685
|
-
text,
|
|
2686
|
-
...asst.uuid ? { messageId: String(asst.uuid) } : {}
|
|
2687
|
-
}
|
|
2688
|
-
)
|
|
2689
|
-
);
|
|
2690
|
-
continue;
|
|
2691
|
-
}
|
|
2692
|
-
if (message.type === "result") {
|
|
2693
|
-
const result = message;
|
|
2694
|
-
lastTerminalReason = result.terminal_reason;
|
|
2695
|
-
lastIsError = result.is_error;
|
|
2696
|
-
const resultText = result.subtype === "success" ? result.result : accumulatedText;
|
|
2697
|
-
if (resultText && resultText !== accumulatedText) {
|
|
2698
|
-
accumulatedText = resultText;
|
|
2699
|
-
}
|
|
2700
|
-
pendingMessages--;
|
|
2701
|
-
if (pendingMessages <= 0) break;
|
|
2887
|
+
const control = item;
|
|
2888
|
+
if (!control._permission) {
|
|
2889
|
+
yield item;
|
|
2702
2890
|
continue;
|
|
2703
2891
|
}
|
|
2892
|
+
const ask = control._permission;
|
|
2893
|
+
const isQuestion = ask.toolName === "AskUserQuestion";
|
|
2894
|
+
const isPlan = ask.toolName === "ExitPlanMode";
|
|
2895
|
+
const answer = await sink.requestPermission({ type: "permission.requested", provider: request.provider, runId: request.runId, timestamp: (/* @__PURE__ */ new Date()).toISOString(), requestId: ask.requestId, kind: isQuestion ? "question" : isPlan ? "plan" : "tool", toolName: ask.toolName, title: isQuestion ? "Your input is needed" : isPlan ? "Review the plan" : ask.title ?? `Allow ${ask.toolName}?`, input: ask.input, ...isQuestion ? { questions: normalizeUserQuestions("claude-code", ask.input) } : {} });
|
|
2896
|
+
const reply = await fetch(`${baseUrl}/runs/${encodeURIComponent(request.runId)}/permission`, { method: "POST", headers: { "content-type": "application/json", ...authHeaders }, signal: fetchAbort.signal, body: JSON.stringify({ requestId: ask.requestId, response: answer.decision === "allow" ? { behavior: "allow", updatedInput: isQuestion ? { ...ask.input, answers: questionReply("claude-code", ask.input, answer.answers ?? []) } : ask.input } : { behavior: "deny", message: "The user declined this request." } }) });
|
|
2897
|
+
if (!reply.ok) throw new Error(`Claude permission response failed: ${reply.status}`);
|
|
2704
2898
|
}
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
const isError = !isCancelled && lastIsError;
|
|
2708
|
-
if (isCancelled) {
|
|
2709
|
-
debugClaude(
|
|
2710
|
-
"\u2605 run.cancelled (%dms since execute start) reason=%s",
|
|
2711
|
-
Date.now() - executeStartedAt,
|
|
2712
|
-
lastTerminalReason
|
|
2713
|
-
);
|
|
2714
|
-
sink.cancel({
|
|
2715
|
-
text: finalText,
|
|
2716
|
-
costData: extractClaudeCostData(rawPayloads)
|
|
2717
|
-
});
|
|
2718
|
-
} else if (isError) {
|
|
2719
|
-
debugClaude(
|
|
2720
|
-
"\u2605 run.error (%dms since execute start) reason=%s",
|
|
2721
|
-
Date.now() - executeStartedAt,
|
|
2722
|
-
lastTerminalReason
|
|
2723
|
-
);
|
|
2724
|
-
sink.fail(
|
|
2725
|
-
new Error(
|
|
2726
|
-
finalText || `claude-code run failed (terminal_reason: ${lastTerminalReason})`
|
|
2727
|
-
)
|
|
2728
|
-
);
|
|
2729
|
-
} else {
|
|
2730
|
-
debugClaude(
|
|
2731
|
-
"\u2605 run.completed (%dms since execute start) chars=%d",
|
|
2732
|
-
Date.now() - executeStartedAt,
|
|
2733
|
-
finalText.length
|
|
2734
|
-
);
|
|
2735
|
-
sink.emitEvent(
|
|
2736
|
-
createNormalizedEvent(
|
|
2737
|
-
"run.completed",
|
|
2738
|
-
{ provider: request.provider, runId: request.runId },
|
|
2739
|
-
{ text: finalText }
|
|
2740
|
-
)
|
|
2741
|
-
);
|
|
2742
|
-
sink.complete({
|
|
2743
|
-
text: finalText,
|
|
2744
|
-
costData: extractClaudeCostData(rawPayloads)
|
|
2745
|
-
});
|
|
2746
|
-
}
|
|
2747
|
-
} finally {
|
|
2748
|
-
fetchAbort.abort();
|
|
2749
|
-
}
|
|
2899
|
+
};
|
|
2900
|
+
await consumeClaudeMessages(request, sink, permissionMessages(), executeStartedAt, cleanup, () => cancelled);
|
|
2750
2901
|
return async () => void 0;
|
|
2751
2902
|
}
|
|
2752
2903
|
/**
|
|
@@ -2806,7 +2957,395 @@ ${serialized}` : serialized;
|
|
|
2806
2957
|
);
|
|
2807
2958
|
}
|
|
2808
2959
|
}
|
|
2809
|
-
};
|
|
2960
|
+
};
|
|
2961
|
+
async function consumeClaudeMessages(request, sink, messages, executeStartedAt, cleanup, wasCancelled = () => false, wait = {}) {
|
|
2962
|
+
let accumulatedText = "";
|
|
2963
|
+
let streamedThinkingChars = 0;
|
|
2964
|
+
let sawResult = false;
|
|
2965
|
+
let firstStreamEventLogged = false;
|
|
2966
|
+
let firstTextDeltaLogged = false;
|
|
2967
|
+
let lastTerminalReason;
|
|
2968
|
+
let lastIsError = false;
|
|
2969
|
+
const rawPayloads = [];
|
|
2970
|
+
const tracker = new BackgroundTaskTracker();
|
|
2971
|
+
const timeoutMs = resolveBackgroundTaskTimeoutMs(request.options.backgroundTaskTimeoutMs);
|
|
2972
|
+
const graceMs = wait.graceMs ?? BACKGROUND_TASK_GRACE_MS;
|
|
2973
|
+
let pendingWait;
|
|
2974
|
+
let waitedMs = 0;
|
|
2975
|
+
let expiry;
|
|
2976
|
+
let lastTasksKey = JSON.stringify({ tasks: [], waiting: false });
|
|
2977
|
+
const emitTasks = (tasks, waiting) => {
|
|
2978
|
+
const key = JSON.stringify({ tasks, waiting });
|
|
2979
|
+
if (key === lastTasksKey) return;
|
|
2980
|
+
lastTasksKey = key;
|
|
2981
|
+
sink.emitEvent(createNormalizedEvent("background.tasks", { provider: request.provider, runId: request.runId }, { tasks, waiting }));
|
|
2982
|
+
};
|
|
2983
|
+
const isAborted = () => wasCancelled() || lastTerminalReason === "aborted_streaming" || lastTerminalReason === "aborted_tools";
|
|
2984
|
+
const endWait = () => {
|
|
2985
|
+
if (!pendingWait) return;
|
|
2986
|
+
waitedMs += pendingWait.elapsedMs();
|
|
2987
|
+
pendingWait.clear();
|
|
2988
|
+
pendingWait = void 0;
|
|
2989
|
+
};
|
|
2990
|
+
const settleOnFailure = (error) => {
|
|
2991
|
+
if (!pendingWait || !sawResult || lastIsError) return false;
|
|
2992
|
+
debugClaude("\u2605 transport failed during background wait; settling on the last result: %o", error);
|
|
2993
|
+
expiry = "transport";
|
|
2994
|
+
return true;
|
|
2995
|
+
};
|
|
2996
|
+
const iterator = messages[Symbol.asyncIterator]();
|
|
2997
|
+
try {
|
|
2998
|
+
for (let next = iterator.next(); ; next = iterator.next()) {
|
|
2999
|
+
let step;
|
|
3000
|
+
try {
|
|
3001
|
+
step = pendingWait ? await Promise.race([
|
|
3002
|
+
next.then((result) => ({ result })),
|
|
3003
|
+
pendingWait.expired.then((reason) => ({ reason }))
|
|
3004
|
+
]) : { result: await next };
|
|
3005
|
+
} catch (error) {
|
|
3006
|
+
if (!settleOnFailure(error)) throw error;
|
|
3007
|
+
break;
|
|
3008
|
+
}
|
|
3009
|
+
if ("reason" in step) {
|
|
3010
|
+
expiry = step.reason;
|
|
3011
|
+
break;
|
|
3012
|
+
}
|
|
3013
|
+
if (step.result.done) break;
|
|
3014
|
+
const item = step.result.value;
|
|
3015
|
+
if (item && typeof item === "object") {
|
|
3016
|
+
const ctrl = item;
|
|
3017
|
+
if ("_error" in ctrl) {
|
|
3018
|
+
const error = new Error(String(ctrl._error ?? "daemon error"));
|
|
3019
|
+
if (!settleOnFailure(error)) throw error;
|
|
3020
|
+
break;
|
|
3021
|
+
}
|
|
3022
|
+
if ("_notice" in ctrl) {
|
|
3023
|
+
debugClaude("daemon notice: %o", ctrl);
|
|
3024
|
+
sink.emitRaw(
|
|
3025
|
+
toRawEvent(
|
|
3026
|
+
request.runId,
|
|
3027
|
+
ctrl,
|
|
3028
|
+
`daemon.${String(ctrl._notice ?? "notice")}`
|
|
3029
|
+
)
|
|
3030
|
+
);
|
|
3031
|
+
continue;
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
const message = item;
|
|
3035
|
+
rawPayloads.push(message);
|
|
3036
|
+
sink.emitRaw(toRawEvent(request.runId, message, message.type));
|
|
3037
|
+
if (tracker.ingest(message) && pendingWait) {
|
|
3038
|
+
debugClaude("\u2605 follow-up turn started; background wait over (%dms since execute start)", Date.now() - executeStartedAt);
|
|
3039
|
+
endWait();
|
|
3040
|
+
}
|
|
3041
|
+
emitTasks(tracker.liveTasks(), pendingWait !== void 0);
|
|
3042
|
+
pendingWait?.setIdle(tracker.liveTasks().length === 0);
|
|
3043
|
+
if (message.type === "system") {
|
|
3044
|
+
const sub = message.subtype;
|
|
3045
|
+
if (sub === "init") {
|
|
3046
|
+
const sys = message;
|
|
3047
|
+
if (request.run.goal && !sys.slash_commands.some((command) => command.replace(/^\//, "") === "goal")) {
|
|
3048
|
+
await cleanup();
|
|
3049
|
+
throw new Error("This Claude Code installation does not expose the native /goal command.");
|
|
3050
|
+
}
|
|
3051
|
+
if (sys.session_id) {
|
|
3052
|
+
debugClaude(
|
|
3053
|
+
"\u2605 session.init session_id=%s (%dms)",
|
|
3054
|
+
sys.session_id.slice(0, 8),
|
|
3055
|
+
Date.now() - executeStartedAt
|
|
3056
|
+
);
|
|
3057
|
+
}
|
|
3058
|
+
} else if (sub === "hook_started") {
|
|
3059
|
+
const h = message;
|
|
3060
|
+
debugClaude(
|
|
3061
|
+
"hook.started name=%s event=%s hook_id=%s",
|
|
3062
|
+
h.hook_name,
|
|
3063
|
+
h.hook_event,
|
|
3064
|
+
h.hook_id
|
|
3065
|
+
);
|
|
3066
|
+
} else if (sub === "hook_response") {
|
|
3067
|
+
const h = message;
|
|
3068
|
+
const stderr = h.stderr && h.stderr.length > 0 ? h.stderr.replace(/\s+$/, "") : void 0;
|
|
3069
|
+
debugClaude(
|
|
3070
|
+
"hook.response name=%s exit=%s outcome=%s%s",
|
|
3071
|
+
h.hook_name,
|
|
3072
|
+
h.exit_code,
|
|
3073
|
+
h.outcome,
|
|
3074
|
+
stderr ? ` stderr=${JSON.stringify(stderr).slice(0, 200)}` : ""
|
|
3075
|
+
);
|
|
3076
|
+
}
|
|
3077
|
+
continue;
|
|
3078
|
+
}
|
|
3079
|
+
if (message.type === "stream_event") {
|
|
3080
|
+
if (!firstStreamEventLogged) {
|
|
3081
|
+
firstStreamEventLogged = true;
|
|
3082
|
+
debugClaude(
|
|
3083
|
+
"\u2605 first stream_event (%dms since execute start)",
|
|
3084
|
+
Date.now() - executeStartedAt
|
|
3085
|
+
);
|
|
3086
|
+
}
|
|
3087
|
+
const partial = message;
|
|
3088
|
+
if (partial.parent_tool_use_id) continue;
|
|
3089
|
+
const streamType = partial.event?.type;
|
|
3090
|
+
if (streamType === "message_start") {
|
|
3091
|
+
accumulatedText = "";
|
|
3092
|
+
streamedThinkingChars = 0;
|
|
3093
|
+
}
|
|
3094
|
+
const { text: text2, thinking } = extractStreamDeltas(partial);
|
|
3095
|
+
if (thinking) {
|
|
3096
|
+
streamedThinkingChars += thinking.length;
|
|
3097
|
+
sink.emitEvent(
|
|
3098
|
+
createNormalizedEvent(
|
|
3099
|
+
"reasoning.delta",
|
|
3100
|
+
{ provider: request.provider, runId: request.runId },
|
|
3101
|
+
{ delta: thinking }
|
|
3102
|
+
)
|
|
3103
|
+
);
|
|
3104
|
+
}
|
|
3105
|
+
if (text2) {
|
|
3106
|
+
if (!firstTextDeltaLogged) {
|
|
3107
|
+
firstTextDeltaLogged = true;
|
|
3108
|
+
debugClaude(
|
|
3109
|
+
"\u2605 first text delta (%dms since execute start)",
|
|
3110
|
+
Date.now() - executeStartedAt
|
|
3111
|
+
);
|
|
3112
|
+
}
|
|
3113
|
+
accumulatedText += text2;
|
|
3114
|
+
sink.emitEvent(
|
|
3115
|
+
createNormalizedEvent(
|
|
3116
|
+
"text.delta",
|
|
3117
|
+
{ provider: request.provider, runId: request.runId },
|
|
3118
|
+
{ delta: text2 }
|
|
3119
|
+
)
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
continue;
|
|
3123
|
+
}
|
|
3124
|
+
if (message.type === "assistant") {
|
|
3125
|
+
const asst = message;
|
|
3126
|
+
if (asst.parent_tool_use_id) continue;
|
|
3127
|
+
const thinking = extractAssistantThinking(asst);
|
|
3128
|
+
if (thinking && streamedThinkingChars === 0) {
|
|
3129
|
+
sink.emitEvent(
|
|
3130
|
+
createNormalizedEvent(
|
|
3131
|
+
"reasoning.delta",
|
|
3132
|
+
{ provider: request.provider, runId: request.runId },
|
|
3133
|
+
{ delta: thinking }
|
|
3134
|
+
)
|
|
3135
|
+
);
|
|
3136
|
+
}
|
|
3137
|
+
const text2 = extractAssistantText(asst);
|
|
3138
|
+
sink.emitEvent(
|
|
3139
|
+
createNormalizedEvent(
|
|
3140
|
+
"message.completed",
|
|
3141
|
+
{ provider: request.provider, runId: request.runId },
|
|
3142
|
+
{
|
|
3143
|
+
text: text2,
|
|
3144
|
+
...asst.uuid ? { messageId: String(asst.uuid) } : {}
|
|
3145
|
+
}
|
|
3146
|
+
)
|
|
3147
|
+
);
|
|
3148
|
+
continue;
|
|
3149
|
+
}
|
|
3150
|
+
if (message.type === "result") {
|
|
3151
|
+
sawResult = true;
|
|
3152
|
+
const result = message;
|
|
3153
|
+
lastTerminalReason = result.terminal_reason;
|
|
3154
|
+
lastIsError = result.is_error;
|
|
3155
|
+
const resultText = result.subtype === "success" ? result.result : accumulatedText;
|
|
3156
|
+
if (resultText && resultText !== accumulatedText) {
|
|
3157
|
+
accumulatedText = resultText;
|
|
3158
|
+
}
|
|
3159
|
+
const live = tracker.liveTasks();
|
|
3160
|
+
if (timeoutMs === 0 || !tracker.hasSeenBackgroundWork() || isAborted()) break;
|
|
3161
|
+
debugClaude("\u2605 turn ended with %d background task(s); waiting", live.length);
|
|
3162
|
+
endWait();
|
|
3163
|
+
pendingWait = new BackgroundWait(graceMs, Math.max(0, timeoutMs - waitedMs));
|
|
3164
|
+
pendingWait.setIdle(live.length === 0);
|
|
3165
|
+
emitTasks(live, true);
|
|
3166
|
+
continue;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
if (expiry === "ceiling") {
|
|
3170
|
+
const ids = tracker.liveTasks().filter((task) => task.type !== "scheduled_wakeup").map((task) => task.id);
|
|
3171
|
+
debugClaude("\u2605 background wait ceiling (%dms) hit; stopping %d task(s)", timeoutMs, ids.length);
|
|
3172
|
+
if (wait.stopTasks) {
|
|
3173
|
+
await withTimeout(wait.stopTasks(ids), wait.stopTimeoutMs ?? STOP_TASKS_TIMEOUT_MS).catch(() => void 0);
|
|
3174
|
+
}
|
|
3175
|
+
} else if (expiry === "grace") {
|
|
3176
|
+
debugClaude("\u2605 background set emptied with no follow-up turn; settling");
|
|
3177
|
+
}
|
|
3178
|
+
if (pendingWait) {
|
|
3179
|
+
endWait();
|
|
3180
|
+
emitTasks([], false);
|
|
3181
|
+
}
|
|
3182
|
+
await cleanup();
|
|
3183
|
+
if (!sawResult && !wasCancelled()) throw new Error("Claude Code closed before reporting a result");
|
|
3184
|
+
const finalText = accumulatedText;
|
|
3185
|
+
const isCancelled = isAborted();
|
|
3186
|
+
const isError = !isCancelled && lastIsError;
|
|
3187
|
+
if (isCancelled) {
|
|
3188
|
+
debugClaude(
|
|
3189
|
+
"\u2605 run.cancelled (%dms since execute start) reason=%s",
|
|
3190
|
+
Date.now() - executeStartedAt,
|
|
3191
|
+
lastTerminalReason
|
|
3192
|
+
);
|
|
3193
|
+
sink.cancel({
|
|
3194
|
+
text: finalText,
|
|
3195
|
+
costData: extractClaudeCostData(rawPayloads)
|
|
3196
|
+
});
|
|
3197
|
+
} else if (isError) {
|
|
3198
|
+
debugClaude(
|
|
3199
|
+
"\u2605 run.error (%dms since execute start) reason=%s",
|
|
3200
|
+
Date.now() - executeStartedAt,
|
|
3201
|
+
lastTerminalReason
|
|
3202
|
+
);
|
|
3203
|
+
sink.fail(
|
|
3204
|
+
new Error(
|
|
3205
|
+
finalText || `claude-code run failed (terminal_reason: ${lastTerminalReason})`
|
|
3206
|
+
)
|
|
3207
|
+
);
|
|
3208
|
+
} else {
|
|
3209
|
+
debugClaude(
|
|
3210
|
+
"\u2605 run.completed (%dms since execute start) chars=%d",
|
|
3211
|
+
Date.now() - executeStartedAt,
|
|
3212
|
+
finalText.length
|
|
3213
|
+
);
|
|
3214
|
+
sink.emitEvent(
|
|
3215
|
+
createNormalizedEvent(
|
|
3216
|
+
"run.completed",
|
|
3217
|
+
{ provider: request.provider, runId: request.runId },
|
|
3218
|
+
{ text: finalText }
|
|
3219
|
+
)
|
|
3220
|
+
);
|
|
3221
|
+
sink.complete({
|
|
3222
|
+
text: finalText,
|
|
3223
|
+
costData: extractClaudeCostData(rawPayloads)
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
} finally {
|
|
3227
|
+
pendingWait?.clear();
|
|
3228
|
+
await cleanup();
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
async function executeNativeClaude(request, sink, wait = {}) {
|
|
3232
|
+
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
3233
|
+
const claudeDir = claudeConfigDir(request.options);
|
|
3234
|
+
const input = await validateProviderUserInput(request.provider, request.run.input);
|
|
3235
|
+
const prompt = new AsyncQueue();
|
|
3236
|
+
const sessionId = request.run.resumeSessionId ?? randomUUID();
|
|
3237
|
+
const controller = new AbortController();
|
|
3238
|
+
let handle;
|
|
3239
|
+
let processHandle;
|
|
3240
|
+
let stopped;
|
|
3241
|
+
let cancelled = false;
|
|
3242
|
+
const stop = () => stopped ??= (async () => {
|
|
3243
|
+
prompt.finish();
|
|
3244
|
+
handle?.close();
|
|
3245
|
+
controller.abort();
|
|
3246
|
+
if (processHandle) await processHandle.kill();
|
|
3247
|
+
})();
|
|
3248
|
+
sink.setAbort(async () => {
|
|
3249
|
+
cancelled = true;
|
|
3250
|
+
await stop();
|
|
3251
|
+
});
|
|
3252
|
+
sink.setSessionId(sessionId);
|
|
3253
|
+
const messageId = randomUUID();
|
|
3254
|
+
prompt.push({ type: "user", uuid: messageId, message: { role: "user", content: mapToClaudeUserContent(input) }, parent_tool_use_id: null });
|
|
3255
|
+
const hostEnv = Object.fromEntries(Object.entries({ ...process.env, ...request.options.env }).filter((entry) => entry[1] !== void 0));
|
|
3256
|
+
applyCliBackgroundWaitCeiling(hostEnv);
|
|
3257
|
+
if (request.options.customHeaders) {
|
|
3258
|
+
const headers = Object.entries(request.options.customHeaders).map(([name, value]) => `${name}: ${value}`).join("\n");
|
|
3259
|
+
hostEnv.ANTHROPIC_CUSTOM_HEADERS = [hostEnv.ANTHROPIC_CUSTOM_HEADERS, headers].filter(Boolean).join("\n");
|
|
3260
|
+
}
|
|
3261
|
+
const options = buildClaudeQueryOptions({
|
|
3262
|
+
request,
|
|
3263
|
+
...request.options.configuration === "native" ? {} : {
|
|
3264
|
+
settingsPath: path8.join(claudeDir, "settings.json"),
|
|
3265
|
+
mcpConfigPath: path8.join(claudeDir, "agentbox-mcp.json")
|
|
3266
|
+
},
|
|
3267
|
+
// Auth is deliberately CLI-owned. Never copy the user's credential files
|
|
3268
|
+
// into the generated configuration directory or a task artifact.
|
|
3269
|
+
env: hostEnv
|
|
3270
|
+
});
|
|
3271
|
+
const autoApprove = shouldAutoApproveClaudeTools(request.options);
|
|
3272
|
+
const interactiveQuestions = hasInteractiveQuestions(request.options);
|
|
3273
|
+
let planning = options.permissionMode === "plan";
|
|
3274
|
+
try {
|
|
3275
|
+
handle = query({ prompt, options: {
|
|
3276
|
+
...options,
|
|
3277
|
+
// Use the SDK-matched CLI by default; an installed CLI is an explicit override.
|
|
3278
|
+
pathToClaudeCodeExecutable: request.options.provider?.binary,
|
|
3279
|
+
abortController: controller,
|
|
3280
|
+
// `sessionId` is rejected alongside `resume` unless `forkSession` is
|
|
3281
|
+
// set, where it names the forked session. Stamping it on forks keeps
|
|
3282
|
+
// the pre-minted id reported via `sink.setSessionId` truthful, so a
|
|
3283
|
+
// later run can resume the fork.
|
|
3284
|
+
...request.run.resumeSessionId ? {} : { sessionId },
|
|
3285
|
+
...request.options.configuration === "native" ? {} : { plugins: [{ type: "local", path: claudeDir }] },
|
|
3286
|
+
spawnClaudeCodeProcess(spawnOptions) {
|
|
3287
|
+
if (cancelled || controller.signal.aborted) throw new Error("Local run was cancelled before startup");
|
|
3288
|
+
processHandle = spawnCommand({ ...spawnOptions, processGroup: request.options.processGroup !== "inherited" });
|
|
3289
|
+
return processHandle.child;
|
|
3290
|
+
},
|
|
3291
|
+
hooks: { ...options.hooks, PreToolUse: [...options.hooks?.PreToolUse ?? [], { hooks: [async (input2) => {
|
|
3292
|
+
if (input2.hook_event_name !== "PreToolUse") return {};
|
|
3293
|
+
planning = input2.permission_mode === "plan";
|
|
3294
|
+
return interactiveQuestions && ["AskUserQuestion", "ExitPlanMode"].includes(input2.tool_name) ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask" } } : {};
|
|
3295
|
+
}] }] },
|
|
3296
|
+
async canUseTool(toolName, input2, context) {
|
|
3297
|
+
if (cancelled || context.signal.aborted) return { behavior: "deny", message: "Run cancelled", interrupt: true };
|
|
3298
|
+
const isQuestion = toolName === "AskUserQuestion";
|
|
3299
|
+
const isPlan = toolName === "ExitPlanMode";
|
|
3300
|
+
if ((isQuestion || isPlan) && !interactiveQuestions) return { behavior: "deny", message: "No interactive user is available." };
|
|
3301
|
+
if (!isQuestion && !isPlan && planning && toolName !== "EnterPlanMode") return { behavior: "deny", message: "Finish planning before requesting write access." };
|
|
3302
|
+
if (!isQuestion && !isPlan && autoApprove) return { behavior: "allow", updatedInput: input2 };
|
|
3303
|
+
try {
|
|
3304
|
+
const response = await sink.requestPermission({
|
|
3305
|
+
type: "permission.requested",
|
|
3306
|
+
provider: request.provider,
|
|
3307
|
+
runId: request.runId,
|
|
3308
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3309
|
+
requestId: context.toolUseID,
|
|
3310
|
+
kind: isQuestion ? "question" : isPlan ? "plan" : "tool",
|
|
3311
|
+
toolName,
|
|
3312
|
+
...isQuestion ? { questions: normalizeUserQuestions("claude-code", input2) } : {},
|
|
3313
|
+
title: isQuestion ? "Your input is needed" : isPlan ? "Review the plan" : context.title ?? `Allow ${toolName}?`,
|
|
3314
|
+
message: context.description ?? context.decisionReason,
|
|
3315
|
+
input: input2,
|
|
3316
|
+
canRemember: false
|
|
3317
|
+
});
|
|
3318
|
+
if (isPlan && response.decision === "allow") planning = false;
|
|
3319
|
+
if (!cancelled && !context.signal.aborted && response.decision === "allow") return {
|
|
3320
|
+
behavior: "allow",
|
|
3321
|
+
updatedInput: isQuestion ? { ...input2, answers: questionReply("claude-code", input2, response.answers ?? []) } : input2
|
|
3322
|
+
};
|
|
3323
|
+
return { behavior: "deny", message: "The user denied this action" };
|
|
3324
|
+
} catch {
|
|
3325
|
+
return { behavior: "deny", message: "Run cancelled", interrupt: true };
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
} });
|
|
3329
|
+
const live = handle;
|
|
3330
|
+
sink.setRaw({ query: live, claudeDir, runId: request.runId });
|
|
3331
|
+
sink.emitEvent(createNormalizedEvent("run.started", { provider: request.provider, runId: request.runId }));
|
|
3332
|
+
sink.emitEvent(createNormalizedEvent("message.started", { provider: request.provider, runId: request.runId }, { messageId }));
|
|
3333
|
+
await consumeClaudeMessages(request, sink, live, Date.now(), stop, () => cancelled, {
|
|
3334
|
+
...wait,
|
|
3335
|
+
// Native owns the CLI: ask it to stop leftover tasks before closing it.
|
|
3336
|
+
stopTasks: async (ids) => {
|
|
3337
|
+
await Promise.all(ids.map((id) => live.stopTask(id).catch(() => void 0)));
|
|
3338
|
+
}
|
|
3339
|
+
});
|
|
3340
|
+
} catch (error) {
|
|
3341
|
+
await stop();
|
|
3342
|
+
if (cancelled) sink.cancel();
|
|
3343
|
+
else throw error;
|
|
3344
|
+
} finally {
|
|
3345
|
+
await stop();
|
|
3346
|
+
}
|
|
3347
|
+
return stop;
|
|
3348
|
+
}
|
|
2810
3349
|
|
|
2811
3350
|
// src/agents/providers/codex.ts
|
|
2812
3351
|
import crypto2 from "crypto";
|
|
@@ -2820,16 +3359,16 @@ async function fetchJson(url, init) {
|
|
|
2820
3359
|
if (!response.ok) {
|
|
2821
3360
|
throw new Error(`Request to ${url} failed with ${response.status}.`);
|
|
2822
3361
|
}
|
|
2823
|
-
const
|
|
2824
|
-
if (
|
|
3362
|
+
const text2 = await response.text();
|
|
3363
|
+
if (text2.length === 0) {
|
|
2825
3364
|
throw new Error(
|
|
2826
3365
|
`Request to ${url} returned status ${response.status} with an empty body.`
|
|
2827
3366
|
);
|
|
2828
3367
|
}
|
|
2829
3368
|
try {
|
|
2830
|
-
return JSON.parse(
|
|
3369
|
+
return JSON.parse(text2);
|
|
2831
3370
|
} catch (error) {
|
|
2832
|
-
const preview =
|
|
3371
|
+
const preview = text2.length > 200 ? `${text2.slice(0, 200)}\u2026` : text2;
|
|
2833
3372
|
const cause = error instanceof Error ? error.message : String(error);
|
|
2834
3373
|
throw new Error(
|
|
2835
3374
|
`Could not parse JSON response from ${url} (status ${response.status}): ${cause}. Body: ${preview}`
|
|
@@ -3030,7 +3569,7 @@ var JsonRpcLineClient = class {
|
|
|
3030
3569
|
// src/agents/providers/codex.ts
|
|
3031
3570
|
function codexConfigDir(options) {
|
|
3032
3571
|
return path9.join(
|
|
3033
|
-
agentboxRoot(AgentProvider.Codex, Boolean(options.sandbox)),
|
|
3572
|
+
agentboxRoot(AgentProvider.Codex, Boolean(options.sandbox), options.stateDirectory),
|
|
3034
3573
|
".codex"
|
|
3035
3574
|
);
|
|
3036
3575
|
}
|
|
@@ -3097,20 +3636,20 @@ function compactEnv(values) {
|
|
|
3097
3636
|
);
|
|
3098
3637
|
}
|
|
3099
3638
|
function buildCodexSandboxMode(options) {
|
|
3100
|
-
return options.sandbox ? "workspace-write" : "read-only";
|
|
3639
|
+
return options.fullAccess ? "danger-full-access" : options.provider?.sandboxMode ?? (options.configuration === "native" ? void 0 : options.sandbox ? "workspace-write" : "read-only");
|
|
3101
3640
|
}
|
|
3102
3641
|
function buildThreadParams(cwd, options, request) {
|
|
3103
3642
|
return {
|
|
3104
3643
|
cwd,
|
|
3105
3644
|
model: request.run.model ?? null,
|
|
3106
|
-
approvalPolicy: isInteractiveApproval(options) ? "untrusted" : "never",
|
|
3645
|
+
...options.provider?.approvalPolicy ? { approvalPolicy: options.provider.approvalPolicy } : options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
|
|
3107
3646
|
sandbox: buildCodexSandboxMode(options),
|
|
3108
3647
|
serviceName: "agentbox",
|
|
3109
3648
|
// Persist the rollout on disk so follow-up runs can call `thread/resume`.
|
|
3110
3649
|
// `ephemeral: true` threads have no rollout file and resume fails with
|
|
3111
3650
|
// "no rollout found for thread id ...".
|
|
3112
3651
|
experimentalRawEvents: true,
|
|
3113
|
-
developerInstructions: request.run.systemPrompt
|
|
3652
|
+
...request.run.systemPrompt ? { developerInstructions: request.run.systemPrompt } : options.configuration === "native" ? {} : { developerInstructions: null }
|
|
3114
3653
|
};
|
|
3115
3654
|
}
|
|
3116
3655
|
function buildResumeParams(cwd, options, request) {
|
|
@@ -3118,9 +3657,9 @@ function buildResumeParams(cwd, options, request) {
|
|
|
3118
3657
|
threadId: request.run.resumeSessionId,
|
|
3119
3658
|
cwd,
|
|
3120
3659
|
model: request.run.model ?? null,
|
|
3121
|
-
approvalPolicy: isInteractiveApproval(options) ? "untrusted" : "never",
|
|
3660
|
+
...options.provider?.approvalPolicy ? { approvalPolicy: options.provider.approvalPolicy } : options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
|
|
3122
3661
|
sandbox: buildCodexSandboxMode(options),
|
|
3123
|
-
developerInstructions: request.run.systemPrompt
|
|
3662
|
+
...request.run.systemPrompt ? { developerInstructions: request.run.systemPrompt } : options.configuration === "native" ? {} : { developerInstructions: null },
|
|
3124
3663
|
// We only need the thread id back; we never read `thread.turns`.
|
|
3125
3664
|
// Without this Codex hydrates the full history into the response and
|
|
3126
3665
|
// emits a `deprecationNotice` ("Full-history hydration is deprecated
|
|
@@ -3134,15 +3673,22 @@ function buildForkParams(cwd, options, request) {
|
|
|
3134
3673
|
lastTurnId: request.run.forkAtMessageId ?? null,
|
|
3135
3674
|
cwd,
|
|
3136
3675
|
model: request.run.model ?? null,
|
|
3137
|
-
approvalPolicy: isInteractiveApproval(options) ? "untrusted" : "never",
|
|
3676
|
+
...options.provider?.approvalPolicy ? { approvalPolicy: options.provider.approvalPolicy } : options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
|
|
3138
3677
|
sandbox: buildCodexSandboxMode(options),
|
|
3139
|
-
developerInstructions: request.run.systemPrompt
|
|
3678
|
+
...request.run.systemPrompt ? { developerInstructions: request.run.systemPrompt } : options.configuration === "native" ? {} : { developerInstructions: null },
|
|
3140
3679
|
excludeTurns: true
|
|
3141
3680
|
};
|
|
3142
3681
|
}
|
|
3143
3682
|
function buildTurnSandboxPolicy(options) {
|
|
3683
|
+
if (options.fullAccess || options.provider?.sandboxMode === "danger-full-access") return { type: "dangerFullAccess" };
|
|
3144
3684
|
if (!options.sandbox) {
|
|
3145
|
-
return void 0;
|
|
3685
|
+
if (buildCodexSandboxMode(options) === void 0) return void 0;
|
|
3686
|
+
if (buildCodexSandboxMode(options) === "read-only") return void 0;
|
|
3687
|
+
return {
|
|
3688
|
+
type: "workspaceWrite",
|
|
3689
|
+
networkAccess: options.provider?.networkAccess ?? false,
|
|
3690
|
+
...options.provider?.writableRoots?.length ? { writableRoots: options.provider.writableRoots } : {}
|
|
3691
|
+
};
|
|
3146
3692
|
}
|
|
3147
3693
|
if (options.sandbox.provider === SandboxProvider.LocalDocker) {
|
|
3148
3694
|
return {
|
|
@@ -3161,13 +3707,53 @@ function buildCodexTurnStartParams(params) {
|
|
|
3161
3707
|
return {
|
|
3162
3708
|
threadId,
|
|
3163
3709
|
input: inputItems,
|
|
3164
|
-
approvalPolicy:
|
|
3710
|
+
...request.options.provider?.approvalPolicy ? { approvalPolicy: request.options.provider.approvalPolicy } : request.options.configuration === "native" && !request.options.fullAccess ? {} : {
|
|
3711
|
+
approvalPolicy: !request.options.fullAccess && isInteractiveApproval(request.options) ? "untrusted" : "never"
|
|
3712
|
+
},
|
|
3165
3713
|
...sandboxPolicy ? { sandboxPolicy } : {},
|
|
3166
3714
|
model: request.run.model ?? null,
|
|
3167
3715
|
effort: request.run.reasoning ?? null,
|
|
3716
|
+
...request.run.mode ? { collaborationMode: {
|
|
3717
|
+
mode: request.run.mode,
|
|
3718
|
+
settings: { model: request.run.model, reasoning_effort: request.run.reasoning ?? null, developer_instructions: null }
|
|
3719
|
+
} } : {},
|
|
3168
3720
|
outputSchema: null
|
|
3169
3721
|
};
|
|
3170
3722
|
}
|
|
3723
|
+
var BACKGROUND_OUTPUT_TAIL_CHARS = 4e3;
|
|
3724
|
+
function buildCodexBackgroundFollowUp(items) {
|
|
3725
|
+
const reports = items.map((item) => {
|
|
3726
|
+
const tail = (item.aggregatedOutput ?? "").slice(-BACKGROUND_OUTPUT_TAIL_CHARS).trimEnd();
|
|
3727
|
+
const duration = item.durationMs === void 0 ? "an unknown time" : `${Math.round(item.durationMs / 1e3)}s`;
|
|
3728
|
+
return `\`${item.command}\` exited with code ${item.exitCode ?? "unknown"} after ${duration}.
|
|
3729
|
+
Output (last ${BACKGROUND_OUTPUT_TAIL_CHARS} chars):
|
|
3730
|
+
\`\`\`
|
|
3731
|
+
${tail || "(no output)"}
|
|
3732
|
+
\`\`\`
|
|
3733
|
+
`;
|
|
3734
|
+
});
|
|
3735
|
+
return `Background command finished while you were idle.
|
|
3736
|
+
|
|
3737
|
+
${reports.join("")}
|
|
3738
|
+
Continue from here: verify the outcome and finish the task. Do not restart the command.`;
|
|
3739
|
+
}
|
|
3740
|
+
var CODEX_CANCEL_TURN_TEXT = "Run cancelled by the host.";
|
|
3741
|
+
async function terminateBackgroundTerminals(client, threadId) {
|
|
3742
|
+
await withTimeout((async () => {
|
|
3743
|
+
try {
|
|
3744
|
+
const listed = await client.request(
|
|
3745
|
+
"thread/backgroundTerminals/list",
|
|
3746
|
+
{ threadId }
|
|
3747
|
+
);
|
|
3748
|
+
for (const terminal of listed?.data ?? []) {
|
|
3749
|
+
if (typeof terminal?.processId !== "string") continue;
|
|
3750
|
+
await client.request("thread/backgroundTerminals/terminate", { threadId, processId: terminal.processId });
|
|
3751
|
+
}
|
|
3752
|
+
} catch (error) {
|
|
3753
|
+
debugCodex("background terminal termination stopped early: %o", error);
|
|
3754
|
+
}
|
|
3755
|
+
})(), STOP_TASKS_TIMEOUT_MS);
|
|
3756
|
+
}
|
|
3171
3757
|
function toRawEvent2(runId, payload, type) {
|
|
3172
3758
|
return {
|
|
3173
3759
|
provider: AgentProvider.Codex,
|
|
@@ -3189,7 +3775,7 @@ function buildCodexCommandArgs(binary, args, options) {
|
|
|
3189
3775
|
overrides.push(["supports_websockets", "false"]);
|
|
3190
3776
|
}
|
|
3191
3777
|
const overrideArgs = overrides.flatMap(([k, v]) => ["-c", `${k}=${v}`]);
|
|
3192
|
-
return ["-u", "XDG_CONFIG_HOME", binary, ...overrideArgs, ...args];
|
|
3778
|
+
return [...options?.configuration === "native" ? [] : ["-u", "XDG_CONFIG_HOME"], binary, ...overrideArgs, ...args];
|
|
3193
3779
|
}
|
|
3194
3780
|
function toNormalizedCodexEvents(runId, notification) {
|
|
3195
3781
|
const base = {
|
|
@@ -3261,8 +3847,8 @@ function toNormalizedCodexEvents(runId, notification) {
|
|
|
3261
3847
|
}
|
|
3262
3848
|
if (notification.method === "turn/completed") {
|
|
3263
3849
|
const turn = notification.params?.turn;
|
|
3264
|
-
const
|
|
3265
|
-
return [createNormalizedEvent("run.completed", base, { text })];
|
|
3850
|
+
const text2 = typeof turn?.lastAgentMessage === "string" ? turn.lastAgentMessage : void 0;
|
|
3851
|
+
return [createNormalizedEvent("run.completed", base, { text: text2 })];
|
|
3266
3852
|
}
|
|
3267
3853
|
if (notification.method === "error") {
|
|
3268
3854
|
const error = notification.params?.error;
|
|
@@ -3274,7 +3860,7 @@ function toNormalizedCodexEvents(runId, notification) {
|
|
|
3274
3860
|
}
|
|
3275
3861
|
return [];
|
|
3276
3862
|
}
|
|
3277
|
-
function createCodexPermissionEvent(request, notification) {
|
|
3863
|
+
function createCodexPermissionEvent(request, notification, fileChanges) {
|
|
3278
3864
|
const raw = toRawEvent2(request.runId, notification, notification.method);
|
|
3279
3865
|
const params = notification.params;
|
|
3280
3866
|
const requestId = notification.id;
|
|
@@ -3317,13 +3903,49 @@ function createCodexPermissionEvent(request, notification) {
|
|
|
3317
3903
|
kind: "file-change",
|
|
3318
3904
|
title: "Approve file changes",
|
|
3319
3905
|
message: typeof params.reason === "string" ? params.reason : "Codex wants to modify files.",
|
|
3320
|
-
input: params,
|
|
3906
|
+
input: fileChanges ? { ...params, changes: fileChanges } : params,
|
|
3321
3907
|
canRemember: availableDecisions.includes("acceptForSession")
|
|
3322
3908
|
}
|
|
3323
3909
|
);
|
|
3324
3910
|
}
|
|
3325
3911
|
return null;
|
|
3326
3912
|
}
|
|
3913
|
+
var CODEX_ELICITATION_METHOD = "mcpServer/elicitation/request";
|
|
3914
|
+
function createCodexElicitationPermissionEvent(request, notification) {
|
|
3915
|
+
if (notification.method !== CODEX_ELICITATION_METHOD || notification.id === void 0) {
|
|
3916
|
+
return null;
|
|
3917
|
+
}
|
|
3918
|
+
const params = notification.params ?? {};
|
|
3919
|
+
const meta = params._meta ?? {};
|
|
3920
|
+
if (meta.codex_approval_kind !== "mcp_tool_call") {
|
|
3921
|
+
return null;
|
|
3922
|
+
}
|
|
3923
|
+
const raw = toRawEvent2(request.runId, notification, notification.method);
|
|
3924
|
+
const toolName = typeof meta.tool_name === "string" ? meta.tool_name : void 0;
|
|
3925
|
+
const server = typeof params.serverName === "string" ? params.serverName : void 0;
|
|
3926
|
+
const persist = Array.isArray(meta.persist) ? meta.persist : [];
|
|
3927
|
+
return createNormalizedEvent(
|
|
3928
|
+
"permission.requested",
|
|
3929
|
+
{ provider: request.provider, runId: request.runId, raw },
|
|
3930
|
+
{
|
|
3931
|
+
requestId: String(notification.id),
|
|
3932
|
+
kind: "tool",
|
|
3933
|
+
toolName: toolName ?? server,
|
|
3934
|
+
title: "Approve tool call",
|
|
3935
|
+
message: typeof params.message === "string" && params.message.trim() ? params.message : `Codex wants to call ${toolName ?? "an MCP tool"}${server ? ` on ${server}` : ""}.`,
|
|
3936
|
+
input: { server, tool: toolName, arguments: meta.tool_params, ...params },
|
|
3937
|
+
canRemember: persist.includes("session")
|
|
3938
|
+
}
|
|
3939
|
+
);
|
|
3940
|
+
}
|
|
3941
|
+
function toCodexElicitationResult(notification, response) {
|
|
3942
|
+
if (response.decision === "deny") {
|
|
3943
|
+
return { action: "decline", content: null };
|
|
3944
|
+
}
|
|
3945
|
+
const meta = notification.params?._meta ?? {};
|
|
3946
|
+
const persist = Array.isArray(meta.persist) ? meta.persist : [];
|
|
3947
|
+
return response.remember && persist.includes("session") ? { action: "accept", content: null, _meta: { persist: "session" } } : { action: "accept", content: null };
|
|
3948
|
+
}
|
|
3327
3949
|
function toCodexApprovalDecision(notification, response) {
|
|
3328
3950
|
const params = notification.params ?? {};
|
|
3329
3951
|
const availableDecisions = Array.isArray(params.availableDecisions) ? params.availableDecisions : [];
|
|
@@ -3371,7 +3993,7 @@ async function materializeCodexImage(options, part, index) {
|
|
|
3371
3993
|
if (data.length === 0) {
|
|
3372
3994
|
throw new Error("Cannot attach an empty image to Codex.");
|
|
3373
3995
|
}
|
|
3374
|
-
const root = agentboxRoot(AgentProvider.Codex, Boolean(options.sandbox));
|
|
3996
|
+
const root = agentboxRoot(AgentProvider.Codex, Boolean(options.sandbox), options.stateDirectory);
|
|
3375
3997
|
const imagePath = path9.join(
|
|
3376
3998
|
root,
|
|
3377
3999
|
"inputs",
|
|
@@ -3518,6 +4140,7 @@ async function connectRemoteCodexAppServer(url, headers = {}) {
|
|
|
3518
4140
|
}
|
|
3519
4141
|
async function setupCodex(request) {
|
|
3520
4142
|
const options = request.options;
|
|
4143
|
+
if (options.configuration === "native") return;
|
|
3521
4144
|
const provider = request.provider;
|
|
3522
4145
|
const hooks = assertHooksSupported(provider, options);
|
|
3523
4146
|
assertCommandsSupported(provider, options.commands);
|
|
@@ -3715,10 +4338,10 @@ async function createRuntime(request, inputParts) {
|
|
|
3715
4338
|
const codexDir = codexConfigDir(options);
|
|
3716
4339
|
const env = compactEnv({
|
|
3717
4340
|
...options.env ?? {},
|
|
3718
|
-
CODEX_HOME: codexDir,
|
|
4341
|
+
...options.configuration === "native" ? {} : { CODEX_HOME: codexDir },
|
|
3719
4342
|
...options.provider?.env ?? {}
|
|
3720
4343
|
});
|
|
3721
|
-
const runtimeCwd = path9.dirname(codexDir);
|
|
4344
|
+
const runtimeCwd = options.configuration === "native" ? options.cwd : path9.dirname(codexDir);
|
|
3722
4345
|
const inputItems = await buildCodexInputItems(options, inputParts);
|
|
3723
4346
|
const usesRemoteWebSocket = options.sandbox && options.sandbox.provider !== SandboxProvider.LocalDocker;
|
|
3724
4347
|
if (usesRemoteWebSocket && options.sandbox) {
|
|
@@ -3788,6 +4411,7 @@ async function createRuntime(request, inputParts) {
|
|
|
3788
4411
|
};
|
|
3789
4412
|
}
|
|
3790
4413
|
const processHandle = spawnCommand({
|
|
4414
|
+
processGroup: options.processGroup !== "inherited",
|
|
3791
4415
|
command: "env",
|
|
3792
4416
|
args: codexArgs,
|
|
3793
4417
|
cwd: runtimeCwd,
|
|
@@ -3796,6 +4420,7 @@ async function createRuntime(request, inputParts) {
|
|
|
3796
4420
|
...env
|
|
3797
4421
|
}
|
|
3798
4422
|
});
|
|
4423
|
+
processHandle.child.stderr.resume();
|
|
3799
4424
|
return {
|
|
3800
4425
|
source: linesFromNodeStream(processHandle.child.stdout),
|
|
3801
4426
|
writeLine: async (line) => {
|
|
@@ -3916,10 +4541,10 @@ var CodexAgentAdapter = class {
|
|
|
3916
4541
|
throw new Error("Cannot send message before thread is started.");
|
|
3917
4542
|
}
|
|
3918
4543
|
const parts = normalizeUserInput(content);
|
|
3919
|
-
const
|
|
4544
|
+
const text2 = parts.filter((p) => p.type === "text").map((p) => p.text).join("");
|
|
3920
4545
|
const inputItems = [];
|
|
3921
|
-
if (
|
|
3922
|
-
inputItems.push({ type: "text", text, text_elements: [] });
|
|
4546
|
+
if (text2.trim().length > 0) {
|
|
4547
|
+
inputItems.push({ type: "text", text: text2, text_elements: [] });
|
|
3923
4548
|
}
|
|
3924
4549
|
const response = await client.request(
|
|
3925
4550
|
"turn/start",
|
|
@@ -3935,11 +4560,95 @@ var CodexAgentAdapter = class {
|
|
|
3935
4560
|
};
|
|
3936
4561
|
sink.onMessage(sendTurn);
|
|
3937
4562
|
const rawPayloads = [];
|
|
4563
|
+
const pendingFileChanges = /* @__PURE__ */ new Map();
|
|
4564
|
+
const fileItemKey = (params, itemId) => typeof params?.threadId === "string" && typeof params.turnId === "string" && typeof itemId === "string" ? `${params.threadId}:${params.turnId}:${itemId}` : void 0;
|
|
3938
4565
|
let streamedText = "";
|
|
4566
|
+
const timeoutMs = resolveBackgroundTaskTimeoutMs(request.options.backgroundTaskTimeoutMs);
|
|
4567
|
+
const isRootThread = (params) => !params?.threadId || params.threadId === rootThreadId;
|
|
4568
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
4569
|
+
const finished = [];
|
|
4570
|
+
let pendingWait;
|
|
4571
|
+
let waitedMs = 0;
|
|
4572
|
+
let followUpSent = false;
|
|
4573
|
+
let turnMessageText = "";
|
|
4574
|
+
let lastTurn;
|
|
4575
|
+
let lastTasksKey = JSON.stringify({ tasks: [], waiting: false });
|
|
4576
|
+
const liveTasks = () => [...inFlight.values()].filter((item) => item.outlived).map((item) => ({ id: item.id, type: "command", description: item.command }));
|
|
4577
|
+
const emitTasks = (tasks, waiting) => {
|
|
4578
|
+
const key = JSON.stringify({ tasks, waiting });
|
|
4579
|
+
if (key === lastTasksKey) return;
|
|
4580
|
+
lastTasksKey = key;
|
|
4581
|
+
sink.emitEvent(createNormalizedEvent("background.tasks", { provider: request.provider, runId: request.runId }, { tasks, waiting }));
|
|
4582
|
+
};
|
|
4583
|
+
const endWait = () => {
|
|
4584
|
+
if (!pendingWait) return;
|
|
4585
|
+
waitedMs += pendingWait.elapsedMs();
|
|
4586
|
+
pendingWait.clear();
|
|
4587
|
+
pendingWait = void 0;
|
|
4588
|
+
followUpSent = false;
|
|
4589
|
+
};
|
|
4590
|
+
const sendBackgroundFollowUp = async () => {
|
|
4591
|
+
if (!rootThreadId || !pendingWait || followUpSent || finished.length === 0 || abortInvoked) return;
|
|
4592
|
+
if (timeoutMs - waitedMs - pendingWait.elapsedMs() <= 0) return;
|
|
4593
|
+
followUpSent = true;
|
|
4594
|
+
const text2 = buildCodexBackgroundFollowUp(finished.splice(0));
|
|
4595
|
+
try {
|
|
4596
|
+
const response = await client.request(
|
|
4597
|
+
"turn/start",
|
|
4598
|
+
buildCodexTurnStartParams({
|
|
4599
|
+
threadId: rootThreadId,
|
|
4600
|
+
inputItems: [{ type: "text", text: text2, text_elements: [] }],
|
|
4601
|
+
request
|
|
4602
|
+
})
|
|
4603
|
+
);
|
|
4604
|
+
endWait();
|
|
4605
|
+
sink.emitEvent(createNormalizedEvent("message.injected", { provider: request.provider, runId: request.runId }, {
|
|
4606
|
+
content: text2,
|
|
4607
|
+
...typeof response?.turn?.id === "string" ? { messageId: response.turn.id } : {}
|
|
4608
|
+
}));
|
|
4609
|
+
} catch (error) {
|
|
4610
|
+
debugCodex("background follow-up turn/start failed: %o", error);
|
|
4611
|
+
}
|
|
4612
|
+
};
|
|
3939
4613
|
const completion = new Promise((resolve, reject) => {
|
|
4614
|
+
const settle = () => {
|
|
4615
|
+
endWait();
|
|
4616
|
+
emitTasks([], false);
|
|
4617
|
+
sink.emitEvent(createNormalizedEvent("run.completed", { provider: request.provider, runId: request.runId }, { text: lastTurn?.messageText || void 0 }));
|
|
4618
|
+
resolve({ text: lastTurn?.text ?? streamedText, turnId, threadId: rootThreadId, interrupted: false });
|
|
4619
|
+
};
|
|
4620
|
+
const settleOnFailure = (error) => {
|
|
4621
|
+
if (!pendingWait || !lastTurn || abortInvoked) return false;
|
|
4622
|
+
debugCodex("\u2605 transport failed during background wait; settling on the last turn: %o", error);
|
|
4623
|
+
settle();
|
|
4624
|
+
return true;
|
|
4625
|
+
};
|
|
3940
4626
|
void (async () => {
|
|
3941
4627
|
let firstClientMessageLogged = false;
|
|
3942
|
-
|
|
4628
|
+
const iterator = client.messages()[Symbol.asyncIterator]();
|
|
4629
|
+
for (let next = iterator.next(); ; next = iterator.next()) {
|
|
4630
|
+
let step;
|
|
4631
|
+
try {
|
|
4632
|
+
step = pendingWait ? await Promise.race([
|
|
4633
|
+
next.then((result) => ({ result })),
|
|
4634
|
+
pendingWait.expired.then((reason) => ({ reason }))
|
|
4635
|
+
]) : { result: await next };
|
|
4636
|
+
} catch (error) {
|
|
4637
|
+
if (settleOnFailure(error)) return;
|
|
4638
|
+
throw error;
|
|
4639
|
+
}
|
|
4640
|
+
if ("reason" in step) {
|
|
4641
|
+
if (!abortInvoked) {
|
|
4642
|
+
debugCodex("\u2605 background wait over (%s) with %d command(s) in flight", step.reason, inFlight.size);
|
|
4643
|
+
if (step.reason === "ceiling" && rootThreadId) await terminateBackgroundTerminals(client, rootThreadId);
|
|
4644
|
+
settle();
|
|
4645
|
+
return;
|
|
4646
|
+
}
|
|
4647
|
+
endWait();
|
|
4648
|
+
step = { result: await next };
|
|
4649
|
+
}
|
|
4650
|
+
if (step.result.done) break;
|
|
4651
|
+
const message = step.result.value;
|
|
3943
4652
|
if (!firstClientMessageLogged) {
|
|
3944
4653
|
firstClientMessageLogged = true;
|
|
3945
4654
|
debugCodex(
|
|
@@ -3951,15 +4660,39 @@ var CodexAgentAdapter = class {
|
|
|
3951
4660
|
const raw = toRawEvent2(request.runId, message, message.method);
|
|
3952
4661
|
rawPayloads.push(message);
|
|
3953
4662
|
sink.emitRaw(raw);
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
4663
|
+
const item = message.params?.item;
|
|
4664
|
+
const itemKey = fileItemKey(message.params, item?.id);
|
|
4665
|
+
if (itemKey && item?.type === "fileChange") {
|
|
4666
|
+
if (message.method === "item/completed") pendingFileChanges.delete(itemKey);
|
|
4667
|
+
else if (message.method === "item/started" && Array.isArray(item.changes)) {
|
|
4668
|
+
if (pendingFileChanges.size >= 128) pendingFileChanges.delete(pendingFileChanges.keys().next().value);
|
|
4669
|
+
pendingFileChanges.set(itemKey, item.changes);
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
if ((message.method === "item/tool/requestUserInput" || message.method === "tool/requestUserInput") && message.id !== void 0) {
|
|
4673
|
+
const questions = normalizeUserQuestions("codex", message.params);
|
|
4674
|
+
const response = hasInteractiveQuestions(request.options) ? await sink.requestPermission(createNormalizedEvent("permission.requested", {
|
|
4675
|
+
provider: request.provider,
|
|
4676
|
+
runId: request.runId,
|
|
4677
|
+
raw
|
|
4678
|
+
}, {
|
|
4679
|
+
requestId: String(message.id),
|
|
4680
|
+
kind: "question",
|
|
4681
|
+
toolName: "request_user_input",
|
|
4682
|
+
title: "Your input is needed",
|
|
4683
|
+
input: message.params,
|
|
4684
|
+
questions,
|
|
4685
|
+
canRemember: false
|
|
4686
|
+
})) : void 0;
|
|
4687
|
+
await client.respond(message.id, { answers: response?.decision === "allow" ? questionReply("codex", message.params, response.answers ?? []) : {} });
|
|
4688
|
+
continue;
|
|
3961
4689
|
}
|
|
3962
|
-
const
|
|
4690
|
+
const approvalKey = fileItemKey(message.params, message.params?.itemId);
|
|
4691
|
+
const permissionEvent = createCodexPermissionEvent(
|
|
4692
|
+
request,
|
|
4693
|
+
message,
|
|
4694
|
+
approvalKey ? pendingFileChanges.get(approvalKey) : void 0
|
|
4695
|
+
);
|
|
3963
4696
|
if (permissionEvent && message.id !== void 0) {
|
|
3964
4697
|
const response = interactiveApproval ? await sink.requestPermission(permissionEvent) : {
|
|
3965
4698
|
requestId: permissionEvent.requestId,
|
|
@@ -3968,32 +4701,99 @@ var CodexAgentAdapter = class {
|
|
|
3968
4701
|
await client.respond(message.id, {
|
|
3969
4702
|
decision: toCodexApprovalDecision(message, response)
|
|
3970
4703
|
});
|
|
4704
|
+
if (approvalKey) pendingFileChanges.delete(approvalKey);
|
|
4705
|
+
continue;
|
|
4706
|
+
}
|
|
4707
|
+
const elicitation = createCodexElicitationPermissionEvent(request, message);
|
|
4708
|
+
if (elicitation && message.id !== void 0) {
|
|
4709
|
+
const response = interactiveApproval ? await sink.requestPermission(elicitation) : { requestId: elicitation.requestId, decision: "allow" };
|
|
4710
|
+
await client.respond(message.id, toCodexElicitationResult(message, response));
|
|
4711
|
+
continue;
|
|
4712
|
+
}
|
|
4713
|
+
if (message.id !== void 0) {
|
|
4714
|
+
debugCodex("unsupported server request %s; declining", message.method);
|
|
4715
|
+
await (message.method === CODEX_ELICITATION_METHOD ? client.respond(message.id, { action: "cancel", content: null }) : client.respondError(message.id, {
|
|
4716
|
+
code: -32601,
|
|
4717
|
+
message: `Unsupported request ${message.method}`
|
|
4718
|
+
}));
|
|
3971
4719
|
continue;
|
|
3972
4720
|
}
|
|
4721
|
+
if (message.method === "item/completed") {
|
|
4722
|
+
const item2 = message.params?.item;
|
|
4723
|
+
if (item2?.type === "plan" && typeof item2.text === "string") {
|
|
4724
|
+
sink.emitEvent(createNormalizedEvent("plan.completed", { provider: request.provider, runId: request.runId }, { text: item2.text }));
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
4727
|
+
const turn = message.params?.turn;
|
|
4728
|
+
const rootTurnCompleted = message.method === "turn/completed" && isRootThread(message.params);
|
|
4729
|
+
const waitAfterTurn = rootTurnCompleted && pendingTurns <= 1 && timeoutMs !== 0 && !abortInvoked && turn?.status === "completed" && (inFlight.size > 0 || finished.length > 0);
|
|
3973
4730
|
for (const event of toNormalizedCodexEvents(request.runId, message)) {
|
|
4731
|
+
if (event.type === "run.completed" && (waitAfterTurn || turn?.status === "interrupted")) continue;
|
|
3974
4732
|
sink.emitEvent(event);
|
|
3975
4733
|
if (event.type === "text.delta") {
|
|
3976
4734
|
streamedText += event.delta;
|
|
4735
|
+
} else if (event.type === "message.completed" && event.text) {
|
|
4736
|
+
turnMessageText = event.text;
|
|
3977
4737
|
}
|
|
3978
4738
|
}
|
|
3979
4739
|
if (message.method === "thread/started" && !rootThreadId) {
|
|
3980
4740
|
rootThreadId = message.params?.thread?.id ?? rootThreadId;
|
|
3981
4741
|
}
|
|
3982
4742
|
if (message.method === "turn/started") {
|
|
3983
|
-
turnId =
|
|
4743
|
+
turnId = turn?.id ?? turnId;
|
|
4744
|
+
if (isRootThread(message.params)) {
|
|
4745
|
+
streamedText = "";
|
|
4746
|
+
turnMessageText = "";
|
|
4747
|
+
if (pendingWait) debugCodex("\u2605 follow-up turn started; background wait over");
|
|
4748
|
+
endWait();
|
|
4749
|
+
emitTasks(liveTasks(), false);
|
|
4750
|
+
}
|
|
3984
4751
|
}
|
|
3985
|
-
if (
|
|
4752
|
+
if (item?.type === "commandExecution" && typeof item.id === "string" && isRootThread(message.params)) {
|
|
4753
|
+
if (message.method === "item/started") {
|
|
4754
|
+
inFlight.set(item.id, {
|
|
4755
|
+
id: item.id,
|
|
4756
|
+
command: String(item.command ?? ""),
|
|
4757
|
+
processId: typeof item.processId === "string" ? item.processId : void 0,
|
|
4758
|
+
outlived: false
|
|
4759
|
+
});
|
|
4760
|
+
} else if (message.method === "item/completed") {
|
|
4761
|
+
const tracked = inFlight.get(item.id);
|
|
4762
|
+
inFlight.delete(item.id);
|
|
4763
|
+
if (tracked?.outlived && pendingWait) {
|
|
4764
|
+
finished.push({
|
|
4765
|
+
...tracked,
|
|
4766
|
+
aggregatedOutput: typeof item.aggregatedOutput === "string" ? item.aggregatedOutput : void 0,
|
|
4767
|
+
exitCode: typeof item.exitCode === "number" ? item.exitCode : void 0,
|
|
4768
|
+
durationMs: typeof item.durationMs === "number" ? item.durationMs : void 0
|
|
4769
|
+
});
|
|
4770
|
+
}
|
|
4771
|
+
emitTasks(liveTasks(), pendingWait !== void 0);
|
|
4772
|
+
pendingWait?.setIdle(inFlight.size === 0);
|
|
4773
|
+
if (pendingWait && inFlight.size === 0) await sendBackgroundFollowUp();
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
if (rootTurnCompleted) {
|
|
3986
4777
|
pendingTurns--;
|
|
3987
4778
|
if (pendingTurns <= 0) {
|
|
3988
|
-
const
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
4779
|
+
const previousText = lastTurn?.text;
|
|
4780
|
+
lastTurn = { text: streamedText, messageText: turnMessageText };
|
|
4781
|
+
if (!waitAfterTurn) {
|
|
4782
|
+
resolve({
|
|
4783
|
+
text: streamedText || previousText,
|
|
4784
|
+
turnId,
|
|
4785
|
+
threadId: rootThreadId,
|
|
4786
|
+
interrupted: turn?.status === "interrupted"
|
|
4787
|
+
});
|
|
4788
|
+
return;
|
|
4789
|
+
}
|
|
4790
|
+
for (const tracked of inFlight.values()) tracked.outlived = true;
|
|
4791
|
+
debugCodex("\u2605 turn ended with %d command(s) in flight; waiting", inFlight.size);
|
|
4792
|
+
endWait();
|
|
4793
|
+
pendingWait = new BackgroundWait(BACKGROUND_TASK_GRACE_MS, Math.max(0, timeoutMs - waitedMs));
|
|
4794
|
+
pendingWait.setIdle(inFlight.size === 0);
|
|
4795
|
+
emitTasks(liveTasks(), true);
|
|
4796
|
+
if (inFlight.size === 0) await sendBackgroundFollowUp();
|
|
3997
4797
|
}
|
|
3998
4798
|
}
|
|
3999
4799
|
if (message.method === "error" && !shouldIgnoreCodexError(message)) {
|
|
@@ -4001,6 +4801,7 @@ var CodexAgentAdapter = class {
|
|
|
4001
4801
|
return;
|
|
4002
4802
|
}
|
|
4003
4803
|
}
|
|
4804
|
+
if (settleOnFailure(new Error("Codex transport closed."))) return;
|
|
4004
4805
|
reject(new Error("Codex transport closed before run completed."));
|
|
4005
4806
|
})().catch(reject);
|
|
4006
4807
|
});
|
|
@@ -4049,6 +4850,13 @@ var CodexAgentAdapter = class {
|
|
|
4049
4850
|
sink.emitRaw(
|
|
4050
4851
|
toRawEvent2(request.runId, threadResponse, threadResultEventName)
|
|
4051
4852
|
);
|
|
4853
|
+
if (request.run.mode) {
|
|
4854
|
+
const modes = await client.request("collaborationMode/list", {});
|
|
4855
|
+
if (!modes.data.some((mode) => mode.mode === request.run.mode)) throw new Error("This Codex installation does not support the requested planning mode.");
|
|
4856
|
+
}
|
|
4857
|
+
if (request.run.goal) {
|
|
4858
|
+
await client.request("thread/goal/set", { threadId: threadResponse.thread.id, objective: request.run.goal, status: "active" });
|
|
4859
|
+
}
|
|
4052
4860
|
await client.request(
|
|
4053
4861
|
"turn/start",
|
|
4054
4862
|
buildCodexTurnStartParams({
|
|
@@ -4064,6 +4872,8 @@ var CodexAgentAdapter = class {
|
|
|
4064
4872
|
} catch (err) {
|
|
4065
4873
|
completionError = err;
|
|
4066
4874
|
}
|
|
4875
|
+
endWait();
|
|
4876
|
+
emitTasks([], false);
|
|
4067
4877
|
if (completionError !== void 0) {
|
|
4068
4878
|
if (abortInvoked) {
|
|
4069
4879
|
debugCodex(
|
|
@@ -4071,31 +4881,32 @@ var CodexAgentAdapter = class {
|
|
|
4071
4881
|
Date.now() - executeStartedAt
|
|
4072
4882
|
);
|
|
4073
4883
|
sink.cancel({
|
|
4074
|
-
text: streamedText || void 0,
|
|
4884
|
+
text: streamedText || lastTurn?.text || void 0,
|
|
4075
4885
|
costData: extractCodexCostData(rawPayloads)
|
|
4076
4886
|
});
|
|
4077
4887
|
} else {
|
|
4078
4888
|
sink.fail(completionError);
|
|
4079
4889
|
}
|
|
4080
4890
|
} else {
|
|
4081
|
-
const { text, interrupted } = completionResult;
|
|
4891
|
+
const { text: text2, interrupted } = completionResult;
|
|
4082
4892
|
if (abortInvoked || interrupted) {
|
|
4083
4893
|
debugCodex(
|
|
4084
4894
|
"\u2605 run.cancelled (%dms since execute start) interrupted=%s",
|
|
4085
4895
|
Date.now() - executeStartedAt,
|
|
4086
4896
|
interrupted
|
|
4087
4897
|
);
|
|
4088
|
-
sink.cancel({ text, costData: extractCodexCostData(rawPayloads) });
|
|
4898
|
+
sink.cancel({ text: text2, costData: extractCodexCostData(rawPayloads) });
|
|
4089
4899
|
} else {
|
|
4090
4900
|
debugCodex(
|
|
4091
4901
|
"\u2605 run.completed (%dms since execute start) chars=%d",
|
|
4092
4902
|
Date.now() - executeStartedAt,
|
|
4093
|
-
|
|
4903
|
+
text2?.length ?? 0
|
|
4094
4904
|
);
|
|
4095
4905
|
sink.complete({ costData: extractCodexCostData(rawPayloads) });
|
|
4096
4906
|
}
|
|
4097
4907
|
}
|
|
4098
4908
|
} finally {
|
|
4909
|
+
pendingWait?.clear();
|
|
4099
4910
|
await runtime.cleanup().catch(() => void 0);
|
|
4100
4911
|
}
|
|
4101
4912
|
return async () => void 0;
|
|
@@ -4108,36 +4919,51 @@ var CodexAgentAdapter = class {
|
|
|
4108
4919
|
* driven by the normalized `message.started` event whose
|
|
4109
4920
|
* `messageId` IS the codex turnId).
|
|
4110
4921
|
*
|
|
4111
|
-
*
|
|
4922
|
+
* When that interrupt is rejected — the thread is idle in a background
|
|
4923
|
+
* wait, or the turn id is stale or missing — a turn is started only to be
|
|
4924
|
+
* interrupted, so the originating run still observes an interrupted turn
|
|
4925
|
+
* and cancels; the model's leftover processes are then terminated.
|
|
4926
|
+
* Without `sessionId` the call is a no-op.
|
|
4112
4927
|
*/
|
|
4113
4928
|
async attachAbort(request) {
|
|
4114
4929
|
const threadId = request.sessionId;
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
debugCodex(
|
|
4118
|
-
"attachAbort runId=%s skipped: threadId=%s turnId=%s",
|
|
4119
|
-
request.runId,
|
|
4120
|
-
threadId,
|
|
4121
|
-
turnId
|
|
4122
|
-
);
|
|
4930
|
+
if (!threadId) {
|
|
4931
|
+
debugCodex("attachAbort runId=%s skipped: no threadId", request.runId);
|
|
4123
4932
|
return;
|
|
4124
4933
|
}
|
|
4125
4934
|
await withCodexAppServer(request, async (client) => {
|
|
4126
|
-
|
|
4127
|
-
|
|
4935
|
+
const bounded = (what, promise) => Promise.race([
|
|
4936
|
+
promise,
|
|
4128
4937
|
new Promise(
|
|
4129
|
-
(_, reject) => setTimeout(
|
|
4130
|
-
() => reject(new Error("codex turn/interrupt timed out")),
|
|
4131
|
-
3e3
|
|
4132
|
-
)
|
|
4938
|
+
(_, reject) => setTimeout(() => reject(new Error(`codex ${what} timed out`)), 3e3)
|
|
4133
4939
|
)
|
|
4134
|
-
])
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4940
|
+
]);
|
|
4941
|
+
const interrupt = (turnId) => bounded("turn/interrupt", client.request("turn/interrupt", { threadId, turnId }));
|
|
4942
|
+
if (request.turnId) {
|
|
4943
|
+
try {
|
|
4944
|
+
await interrupt(request.turnId);
|
|
4945
|
+
return;
|
|
4946
|
+
} catch (error) {
|
|
4947
|
+
debugCodex("attachAbort runId=%s turn/interrupt failed: %o", request.runId, error);
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
try {
|
|
4951
|
+
const response = await bounded(
|
|
4952
|
+
"turn/start",
|
|
4953
|
+
client.request("turn/start", {
|
|
4954
|
+
threadId,
|
|
4955
|
+
input: [{ type: "text", text: CODEX_CANCEL_TURN_TEXT, text_elements: [] }],
|
|
4956
|
+
approvalPolicy: "never",
|
|
4957
|
+
model: null,
|
|
4958
|
+
effort: null,
|
|
4959
|
+
outputSchema: null
|
|
4960
|
+
})
|
|
4139
4961
|
);
|
|
4140
|
-
|
|
4962
|
+
if (typeof response?.turn?.id === "string") await interrupt(response.turn.id);
|
|
4963
|
+
} catch (error) {
|
|
4964
|
+
debugCodex("attachAbort runId=%s cancel turn failed: %o", request.runId, error);
|
|
4965
|
+
}
|
|
4966
|
+
await terminateBackgroundTerminals(client, threadId);
|
|
4141
4967
|
});
|
|
4142
4968
|
}
|
|
4143
4969
|
/**
|
|
@@ -4152,14 +4978,14 @@ var CodexAgentAdapter = class {
|
|
|
4152
4978
|
);
|
|
4153
4979
|
}
|
|
4154
4980
|
const parts = normalizeUserInput(content);
|
|
4155
|
-
const
|
|
4981
|
+
const text2 = joinTextParts(
|
|
4156
4982
|
parts.filter(
|
|
4157
4983
|
(part) => part.type === "text"
|
|
4158
4984
|
)
|
|
4159
4985
|
);
|
|
4160
4986
|
const inputItems = [];
|
|
4161
|
-
if (
|
|
4162
|
-
inputItems.push({ type: "text", text, text_elements: [] });
|
|
4987
|
+
if (text2.trim().length > 0) {
|
|
4988
|
+
inputItems.push({ type: "text", text: text2, text_elements: [] });
|
|
4163
4989
|
}
|
|
4164
4990
|
await withCodexAppServer(request, async (client) => {
|
|
4165
4991
|
await client.request("turn/start", {
|
|
@@ -4175,10 +5001,10 @@ var CodexAgentAdapter = class {
|
|
|
4175
5001
|
};
|
|
4176
5002
|
|
|
4177
5003
|
// src/agents/providers/opencode.ts
|
|
4178
|
-
import { createHash as createHash2 } from "crypto";
|
|
5004
|
+
import { createHash as createHash2, randomBytes, randomUUID as randomUUID2 } from "crypto";
|
|
5005
|
+
import os2 from "os";
|
|
4179
5006
|
import path10 from "path";
|
|
4180
5007
|
var SANDBOX_OPENCODE_PORT = 4096;
|
|
4181
|
-
var LOCAL_OPENCODE_PORT = 4096;
|
|
4182
5008
|
var SANDBOX_OPENCODE_READY_TIMEOUT_MS = 9e4;
|
|
4183
5009
|
var LOCAL_OPENCODE_READY_TIMEOUT_MS = 2e4;
|
|
4184
5010
|
var SHARED_OPENCODE_TARGET_ID = "shared-opencode-server";
|
|
@@ -4230,30 +5056,13 @@ function hashLlmApiKeys(env) {
|
|
|
4230
5056
|
}
|
|
4231
5057
|
return hasher.digest("hex");
|
|
4232
5058
|
}
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
]
|
|
4241
|
-
});
|
|
4242
|
-
await killer.wait().catch(() => void 0);
|
|
4243
|
-
await waitFor(
|
|
4244
|
-
async () => {
|
|
4245
|
-
try {
|
|
4246
|
-
const res = await fetch(
|
|
4247
|
-
`http://127.0.0.1:${LOCAL_OPENCODE_PORT}/global/health`
|
|
4248
|
-
);
|
|
4249
|
-
return !res.ok;
|
|
4250
|
-
} catch {
|
|
4251
|
-
return true;
|
|
4252
|
-
}
|
|
4253
|
-
},
|
|
4254
|
-
{ timeoutMs: 5e3, intervalMs: 200 }
|
|
4255
|
-
).catch(() => void 0);
|
|
4256
|
-
});
|
|
5059
|
+
var localOpenCodeServers = /* @__PURE__ */ new WeakMap();
|
|
5060
|
+
async function killLocalOpenCodeServer(options) {
|
|
5061
|
+
const pending = localOpenCodeServers.get(options);
|
|
5062
|
+
if (!pending) return;
|
|
5063
|
+
const server = await pending;
|
|
5064
|
+
await server.process.kill();
|
|
5065
|
+
localOpenCodeServers.delete(options);
|
|
4257
5066
|
}
|
|
4258
5067
|
async function killSandboxOpenCodeServer(sandbox, pidFilePath, cwd, port) {
|
|
4259
5068
|
await time(debugOpencode, "kill sandbox opencode server", async () => {
|
|
@@ -4283,6 +5092,9 @@ function toRawEvent3(runId, payload, type) {
|
|
|
4283
5092
|
payload
|
|
4284
5093
|
};
|
|
4285
5094
|
}
|
|
5095
|
+
function injectedTaskResultChild(text2) {
|
|
5096
|
+
return /<task id="?([^"\s>]+)"? state="?(?:completed|error)"?>/.exec(text2)?.[1];
|
|
5097
|
+
}
|
|
4286
5098
|
function toOpenCodeModel(model) {
|
|
4287
5099
|
if (!model) {
|
|
4288
5100
|
return void 0;
|
|
@@ -4332,6 +5144,7 @@ function createOpenCodePermissionEvent(request, raw, payload) {
|
|
|
4332
5144
|
},
|
|
4333
5145
|
{
|
|
4334
5146
|
requestId: String(properties.id ?? ""),
|
|
5147
|
+
toolName: permission,
|
|
4335
5148
|
kind: permission === "bash" ? "bash" : permission === "edit" ? "edit" : permission === "external_directory" ? "file-change" : permission === "webfetch" ? "network" : permission === "task" ? "tool" : "unknown",
|
|
4336
5149
|
title: `Approve ${permission} permission`,
|
|
4337
5150
|
message: typeof properties.metadata === "object" && properties.metadata !== null ? JSON.stringify(properties.metadata) : `OpenCode requested ${permission} permission.`,
|
|
@@ -4353,6 +5166,7 @@ function buildOpenCodeConfig(options, interactiveApproval) {
|
|
|
4353
5166
|
prompt: options.systemPrompt || FALLBACK_OPEN_CODE_AGENT_PROMPT,
|
|
4354
5167
|
permission: buildOpenCodePermissionConfig(interactiveApproval),
|
|
4355
5168
|
tools: {
|
|
5169
|
+
question: hasInteractiveQuestions(options),
|
|
4356
5170
|
write: true,
|
|
4357
5171
|
edit: true,
|
|
4358
5172
|
bash: true,
|
|
@@ -4407,7 +5221,7 @@ async function ensureSandboxOpenCodeServer(request) {
|
|
|
4407
5221
|
const port = SANDBOX_OPENCODE_PORT;
|
|
4408
5222
|
const plugins = assertHooksSupported(request.provider, options);
|
|
4409
5223
|
assertCommandsSupported(request.provider, options.commands);
|
|
4410
|
-
const interactiveApproval = isInteractiveApproval(options);
|
|
5224
|
+
const interactiveApproval = !options.fullAccess && isInteractiveApproval(options);
|
|
4411
5225
|
const target = await createSetupTarget(
|
|
4412
5226
|
request.provider,
|
|
4413
5227
|
SHARED_OPENCODE_TARGET_ID,
|
|
@@ -4475,7 +5289,8 @@ async function ensureSandboxOpenCodeServer(request) {
|
|
|
4475
5289
|
const commonEnv = {
|
|
4476
5290
|
OPENCODE_CONFIG: configPath,
|
|
4477
5291
|
OPENCODE_CONFIG_DIR: target.layout.opencodeDir,
|
|
4478
|
-
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
|
|
5292
|
+
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
|
|
5293
|
+
OPENCODE_ENABLE_QUESTION_TOOL: hasInteractiveQuestions(options) ? "true" : "false"
|
|
4479
5294
|
};
|
|
4480
5295
|
await applyDifferentialSetup(target, allArtifacts, installCommands);
|
|
4481
5296
|
if (enableRtk) {
|
|
@@ -4604,85 +5419,91 @@ ${lastLog}` : "")
|
|
|
4604
5419
|
await markSetupComplete(target, setupId);
|
|
4605
5420
|
});
|
|
4606
5421
|
}
|
|
4607
|
-
async function
|
|
4608
|
-
const
|
|
4609
|
-
const
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
const target = await createSetupTarget(
|
|
4613
|
-
request.provider,
|
|
4614
|
-
"shared-setup",
|
|
4615
|
-
options
|
|
4616
|
-
);
|
|
4617
|
-
const { artifacts: skillArtifacts, installCommands } = await prepareSkillArtifacts(
|
|
4618
|
-
request.provider,
|
|
4619
|
-
options.skills,
|
|
4620
|
-
target.layout
|
|
4621
|
-
);
|
|
4622
|
-
const pluginArtifacts = buildOpenCodePluginArtifacts(
|
|
4623
|
-
plugins,
|
|
4624
|
-
target.layout.opencodeDir
|
|
4625
|
-
);
|
|
4626
|
-
const configPath = path10.join(target.layout.opencodeDir, "agentbox.json");
|
|
4627
|
-
const openCodeConfig = buildOpenCodeConfig(options, interactiveApproval);
|
|
4628
|
-
const commonEnv = {
|
|
4629
|
-
OPENCODE_CONFIG: configPath,
|
|
4630
|
-
OPENCODE_CONFIG_DIR: target.layout.opencodeDir,
|
|
4631
|
-
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
|
|
4632
|
-
};
|
|
4633
|
-
const allArtifacts = [
|
|
4634
|
-
...skillArtifacts,
|
|
4635
|
-
...pluginArtifacts,
|
|
4636
|
-
{
|
|
4637
|
-
path: configPath,
|
|
4638
|
-
content: JSON.stringify(openCodeConfig, null, 2)
|
|
4639
|
-
}
|
|
4640
|
-
];
|
|
4641
|
-
const daemonInfo = {
|
|
4642
|
-
port: LOCAL_OPENCODE_PORT,
|
|
4643
|
-
healthPath: "/global/health"
|
|
5422
|
+
async function startLocalOpenCodeServer(request) {
|
|
5423
|
+
const originalOptions = request.options;
|
|
5424
|
+
const options = {
|
|
5425
|
+
...originalOptions,
|
|
5426
|
+
stateDirectory: path10.join(originalOptions.stateDirectory ?? path10.join(os2.tmpdir(), "agentbox-native"), "instances", randomUUID2())
|
|
4644
5427
|
};
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
5428
|
+
let generatedEnv = {};
|
|
5429
|
+
if (options.configuration !== "native") {
|
|
5430
|
+
const plugins = assertHooksSupported(request.provider, options);
|
|
5431
|
+
assertCommandsSupported(request.provider, options.commands);
|
|
5432
|
+
const target = await createSetupTarget(request.provider, "shared-setup", options);
|
|
5433
|
+
const { artifacts: skillArtifacts, installCommands } = await prepareSkillArtifacts(request.provider, options.skills, target.layout);
|
|
5434
|
+
const configPath = path10.join(target.layout.opencodeDir, "agentbox.json");
|
|
5435
|
+
const artifacts = [
|
|
5436
|
+
...skillArtifacts,
|
|
5437
|
+
...buildOpenCodePluginArtifacts(plugins, target.layout.opencodeDir),
|
|
5438
|
+
{ path: configPath, content: JSON.stringify(buildOpenCodeConfig(options, isInteractiveApproval(options)), null, 2) }
|
|
5439
|
+
];
|
|
5440
|
+
await applyDifferentialSetup(target, artifacts, installCommands);
|
|
5441
|
+
generatedEnv = {
|
|
5442
|
+
OPENCODE_CONFIG: configPath,
|
|
5443
|
+
OPENCODE_CONFIG_DIR: target.layout.opencodeDir,
|
|
5444
|
+
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
|
|
5445
|
+
OPENCODE_ENABLE_QUESTION_TOOL: hasInteractiveQuestions(options) ? "true" : "false"
|
|
5446
|
+
};
|
|
4660
5447
|
}
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
5448
|
+
const port = await getAvailablePort();
|
|
5449
|
+
const password = randomBytes(32).toString("base64url");
|
|
5450
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
5451
|
+
const headers = { Authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` };
|
|
5452
|
+
const processHandle = spawnCommand({
|
|
4665
5453
|
command: options.provider?.binary ?? "opencode",
|
|
4666
|
-
args: [
|
|
4667
|
-
"serve",
|
|
4668
|
-
"--hostname",
|
|
4669
|
-
"127.0.0.1",
|
|
4670
|
-
"--port",
|
|
4671
|
-
String(LOCAL_OPENCODE_PORT),
|
|
4672
|
-
...options.provider?.args ?? []
|
|
4673
|
-
],
|
|
5454
|
+
args: ["serve", ...options.provider?.args ?? [], "--hostname", "127.0.0.1", "--port", String(port)],
|
|
4674
5455
|
cwd: options.cwd,
|
|
5456
|
+
processGroup: options.processGroup !== "inherited",
|
|
4675
5457
|
env: {
|
|
4676
5458
|
...process.env,
|
|
4677
|
-
...options.env
|
|
4678
|
-
...
|
|
5459
|
+
...options.env,
|
|
5460
|
+
...generatedEnv,
|
|
5461
|
+
...options.fullAccess ? { OPENCODE_PERMISSION: JSON.stringify({ "*": "allow", question: "ask" }) } : {},
|
|
5462
|
+
...options.interactiveQuestions === true ? { OPENCODE_ENABLE_QUESTION_TOOL: "true" } : {},
|
|
5463
|
+
OPENCODE_SERVER_USERNAME: "opencode",
|
|
5464
|
+
OPENCODE_SERVER_PASSWORD: password
|
|
4679
5465
|
}
|
|
4680
5466
|
});
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
5467
|
+
processHandle.child.stdout.resume();
|
|
5468
|
+
processHandle.child.stderr.resume();
|
|
5469
|
+
try {
|
|
5470
|
+
let startupError;
|
|
5471
|
+
void processHandle.wait().then(
|
|
5472
|
+
(code) => {
|
|
5473
|
+
startupError = new Error(`Local OpenCode server exited before startup (${code})`);
|
|
5474
|
+
},
|
|
5475
|
+
(error) => {
|
|
5476
|
+
startupError = error instanceof Error ? error : new Error(String(error));
|
|
5477
|
+
}
|
|
5478
|
+
);
|
|
5479
|
+
await waitFor(async () => {
|
|
5480
|
+
if (startupError) throw startupError;
|
|
5481
|
+
try {
|
|
5482
|
+
return (await fetch(`${baseUrl}/global/health`, { headers, signal: AbortSignal.timeout(1e3) })).ok;
|
|
5483
|
+
} catch {
|
|
5484
|
+
return false;
|
|
5485
|
+
}
|
|
5486
|
+
}, { timeoutMs: LOCAL_OPENCODE_READY_TIMEOUT_MS });
|
|
5487
|
+
const unauthenticated = await fetch(`${baseUrl}/global/health`, { signal: AbortSignal.timeout(3e3) });
|
|
5488
|
+
if (unauthenticated.status !== 401) throw new Error("This OpenCode version does not enforce local server authentication. Upgrade OpenCode before running it through AgentBox.");
|
|
5489
|
+
return { baseUrl, headers, process: processHandle };
|
|
5490
|
+
} catch (error) {
|
|
5491
|
+
await processHandle.kill();
|
|
5492
|
+
throw error;
|
|
5493
|
+
}
|
|
5494
|
+
}
|
|
5495
|
+
async function ensureLocalOpenCodeServer(request) {
|
|
5496
|
+
let pending = localOpenCodeServers.get(request.options);
|
|
5497
|
+
if (!pending) {
|
|
5498
|
+
pending = startLocalOpenCodeServer(request);
|
|
5499
|
+
localOpenCodeServers.set(request.options, pending);
|
|
5500
|
+
}
|
|
5501
|
+
try {
|
|
5502
|
+
await pending;
|
|
5503
|
+
} catch (error) {
|
|
5504
|
+
localOpenCodeServers.delete(request.options);
|
|
5505
|
+
throw error;
|
|
5506
|
+
}
|
|
4686
5507
|
}
|
|
4687
5508
|
async function setupOpenCode(request) {
|
|
4688
5509
|
if (request.options.sandbox) {
|
|
@@ -4695,16 +5516,6 @@ async function isSandboxOpenCodeServerHealthy(sandbox, cwd, port) {
|
|
|
4695
5516
|
const probe = await sandbox.run(opencodeHealthCurl(port), { cwd, timeoutMs: 5e3 }).catch(() => void 0);
|
|
4696
5517
|
return probe?.exitCode === 0;
|
|
4697
5518
|
}
|
|
4698
|
-
async function isLocalOpenCodeServerHealthy() {
|
|
4699
|
-
try {
|
|
4700
|
-
const res = await fetch(
|
|
4701
|
-
`http://127.0.0.1:${LOCAL_OPENCODE_PORT}/global/health`
|
|
4702
|
-
);
|
|
4703
|
-
return res.ok;
|
|
4704
|
-
} catch {
|
|
4705
|
-
return false;
|
|
4706
|
-
}
|
|
4707
|
-
}
|
|
4708
5519
|
async function killOpenCodeServer(request) {
|
|
4709
5520
|
const { options } = request;
|
|
4710
5521
|
if (options.sandbox) {
|
|
@@ -4725,24 +5536,22 @@ async function killOpenCodeServer(request) {
|
|
|
4725
5536
|
);
|
|
4726
5537
|
return;
|
|
4727
5538
|
}
|
|
4728
|
-
await killLocalOpenCodeServer();
|
|
5539
|
+
await killLocalOpenCodeServer(options);
|
|
4729
5540
|
}
|
|
4730
5541
|
async function buildOpenCodeRuntime(options) {
|
|
4731
5542
|
if (options.sandbox) {
|
|
4732
5543
|
const sandbox = options.sandbox;
|
|
4733
|
-
const
|
|
5544
|
+
const baseUrl = (await sandbox.getPreviewLink(SANDBOX_OPENCODE_PORT)).replace(/\/$/, "");
|
|
4734
5545
|
return {
|
|
4735
|
-
baseUrl
|
|
5546
|
+
baseUrl,
|
|
4736
5547
|
previewHeaders: await opencodeAuthHeaders(sandbox),
|
|
4737
|
-
raw: { baseUrl
|
|
5548
|
+
raw: { baseUrl, port: SANDBOX_OPENCODE_PORT }
|
|
4738
5549
|
};
|
|
4739
5550
|
}
|
|
4740
|
-
const
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
raw: { baseUrl, port: LOCAL_OPENCODE_PORT }
|
|
4745
|
-
};
|
|
5551
|
+
const pending = localOpenCodeServers.get(options);
|
|
5552
|
+
if (!pending) throw new Error("Local OpenCode server has not been set up by this Agent instance");
|
|
5553
|
+
const server = await pending;
|
|
5554
|
+
return { baseUrl: server.baseUrl, previewHeaders: server.headers, raw: { baseUrl: server.baseUrl } };
|
|
4746
5555
|
}
|
|
4747
5556
|
var OpenCodeAgentAdapter = class {
|
|
4748
5557
|
async setup(request) {
|
|
@@ -4811,37 +5620,151 @@ var OpenCodeAgentAdapter = class {
|
|
|
4811
5620
|
resolveSessionTerminal = resolve;
|
|
4812
5621
|
});
|
|
4813
5622
|
let lastSseActivityAt = Date.now();
|
|
5623
|
+
const postAbort = async (id) => {
|
|
5624
|
+
try {
|
|
5625
|
+
await Promise.race([
|
|
5626
|
+
fetchJson(`${runtime.baseUrl}/session/${id}/abort`, {
|
|
5627
|
+
method: "POST",
|
|
5628
|
+
headers: {
|
|
5629
|
+
"content-type": "application/json",
|
|
5630
|
+
...runtime.previewHeaders
|
|
5631
|
+
}
|
|
5632
|
+
}),
|
|
5633
|
+
new Promise(
|
|
5634
|
+
(_, reject) => setTimeout(
|
|
5635
|
+
() => reject(new Error("opencode POST /session/abort timed out")),
|
|
5636
|
+
3e3
|
|
5637
|
+
)
|
|
5638
|
+
)
|
|
5639
|
+
]);
|
|
5640
|
+
} catch {
|
|
5641
|
+
}
|
|
5642
|
+
};
|
|
4814
5643
|
let userAbortRequested = false;
|
|
4815
5644
|
sink.setAbort(async () => {
|
|
4816
5645
|
userAbortRequested = true;
|
|
4817
5646
|
const sessionIdAtAbort = capturedSessionId;
|
|
4818
|
-
if (sessionIdAtAbort)
|
|
4819
|
-
try {
|
|
4820
|
-
await Promise.race([
|
|
4821
|
-
fetchJson(
|
|
4822
|
-
`${runtime.baseUrl}/session/${sessionIdAtAbort}/abort`,
|
|
4823
|
-
{
|
|
4824
|
-
method: "POST",
|
|
4825
|
-
headers: {
|
|
4826
|
-
"content-type": "application/json",
|
|
4827
|
-
...runtime.previewHeaders
|
|
4828
|
-
}
|
|
4829
|
-
}
|
|
4830
|
-
),
|
|
4831
|
-
new Promise(
|
|
4832
|
-
(_, reject) => setTimeout(
|
|
4833
|
-
() => reject(new Error("opencode POST /session/abort timed out")),
|
|
4834
|
-
3e3
|
|
4835
|
-
)
|
|
4836
|
-
)
|
|
4837
|
-
]);
|
|
4838
|
-
} catch {
|
|
4839
|
-
}
|
|
4840
|
-
}
|
|
5647
|
+
if (sessionIdAtAbort) await postAbort(sessionIdAtAbort);
|
|
4841
5648
|
resolveSessionTerminal();
|
|
4842
5649
|
});
|
|
5650
|
+
const backgroundTimeoutMs = resolveBackgroundTaskTimeoutMs(
|
|
5651
|
+
request.options.backgroundTaskTimeoutMs
|
|
5652
|
+
);
|
|
5653
|
+
const trackChildren = backgroundTimeoutMs !== 0;
|
|
5654
|
+
const children = /* @__PURE__ */ new Map();
|
|
5655
|
+
const liveChildren = () => [...children.values()].filter((child) => child.live).map((child) => ({
|
|
5656
|
+
id: child.id,
|
|
5657
|
+
type: "subagent",
|
|
5658
|
+
description: child.title
|
|
5659
|
+
}));
|
|
5660
|
+
const shouldWait = () => [...children.values()].some((child) => child.background) && liveChildren().length > 0;
|
|
5661
|
+
let pendingWait;
|
|
5662
|
+
let waitedMs = 0;
|
|
5663
|
+
let expiry;
|
|
5664
|
+
let sawParentIdle = false;
|
|
5665
|
+
let lastTasksKey = JSON.stringify({ tasks: [], waiting: false });
|
|
5666
|
+
const emitTasks = (tasks, waiting) => {
|
|
5667
|
+
const key = JSON.stringify({ tasks, waiting });
|
|
5668
|
+
if (key === lastTasksKey) return;
|
|
5669
|
+
lastTasksKey = key;
|
|
5670
|
+
sink.emitEvent(
|
|
5671
|
+
createNormalizedEvent(
|
|
5672
|
+
"background.tasks",
|
|
5673
|
+
{ provider: request.provider, runId: request.runId },
|
|
5674
|
+
{ tasks, waiting }
|
|
5675
|
+
)
|
|
5676
|
+
);
|
|
5677
|
+
};
|
|
5678
|
+
const endWait = () => {
|
|
5679
|
+
if (!pendingWait) return;
|
|
5680
|
+
waitedMs += pendingWait.elapsedMs();
|
|
5681
|
+
pendingWait.clear();
|
|
5682
|
+
pendingWait = void 0;
|
|
5683
|
+
};
|
|
5684
|
+
const onChildrenChanged = () => {
|
|
5685
|
+
if (!pendingWait) return;
|
|
5686
|
+
const live = liveChildren();
|
|
5687
|
+
emitTasks(live, true);
|
|
5688
|
+
pendingWait.setIdle(live.length === 0);
|
|
5689
|
+
};
|
|
5690
|
+
const registerChild = (id, title, background = false) => {
|
|
5691
|
+
const known = children.get(id);
|
|
5692
|
+
if (known) {
|
|
5693
|
+
known.background ||= background;
|
|
5694
|
+
if (title && title !== known.title) {
|
|
5695
|
+
known.title = title;
|
|
5696
|
+
onChildrenChanged();
|
|
5697
|
+
}
|
|
5698
|
+
return;
|
|
5699
|
+
}
|
|
5700
|
+
children.set(id, { id, title: title ?? "", live: true, background });
|
|
5701
|
+
onChildrenChanged();
|
|
5702
|
+
};
|
|
5703
|
+
const setChildLive = (id, live) => {
|
|
5704
|
+
const child = children.get(id);
|
|
5705
|
+
if (!child || child.live === live) return;
|
|
5706
|
+
child.live = live;
|
|
5707
|
+
onChildrenChanged();
|
|
5708
|
+
};
|
|
5709
|
+
const reconcileChildren = async () => {
|
|
5710
|
+
const live = [...children.values()].filter((child) => child.live);
|
|
5711
|
+
if (live.length === 0) return;
|
|
5712
|
+
const statuses = await withTimeout(
|
|
5713
|
+
fetchJson(
|
|
5714
|
+
`${runtime.baseUrl}/session/status`,
|
|
5715
|
+
{ headers: runtime.previewHeaders }
|
|
5716
|
+
).catch(() => void 0),
|
|
5717
|
+
3e3
|
|
5718
|
+
);
|
|
5719
|
+
if (!statuses || typeof statuses !== "object") return;
|
|
5720
|
+
for (const child of live) {
|
|
5721
|
+
const status = statuses[child.id];
|
|
5722
|
+
if (!status || status.type === "idle") setChildLive(child.id, false);
|
|
5723
|
+
}
|
|
5724
|
+
};
|
|
5725
|
+
const onParentIdle = async () => {
|
|
5726
|
+
sawParentIdle = true;
|
|
5727
|
+
if (pendingWait || sessionIdleFromSse) return;
|
|
5728
|
+
const tracking = trackChildren && !userAbortRequested;
|
|
5729
|
+
if (tracking && shouldWait()) await reconcileChildren();
|
|
5730
|
+
if (!tracking || !shouldWait()) {
|
|
5731
|
+
sessionIdleFromSse = true;
|
|
5732
|
+
resolveSessionTerminal();
|
|
5733
|
+
return;
|
|
5734
|
+
}
|
|
5735
|
+
const live = liveChildren();
|
|
5736
|
+
debugOpencode(
|
|
5737
|
+
"\u2605 parent idle with %d live subagent(s); waiting",
|
|
5738
|
+
live.length
|
|
5739
|
+
);
|
|
5740
|
+
const wait = new BackgroundWait(
|
|
5741
|
+
BACKGROUND_TASK_GRACE_MS,
|
|
5742
|
+
Math.max(0, backgroundTimeoutMs - waitedMs)
|
|
5743
|
+
);
|
|
5744
|
+
pendingWait = wait;
|
|
5745
|
+
void wait.expired.then((reason) => {
|
|
5746
|
+
if (pendingWait !== wait) return;
|
|
5747
|
+
expiry = reason;
|
|
5748
|
+
resolveSessionTerminal();
|
|
5749
|
+
});
|
|
5750
|
+
emitTasks(live, true);
|
|
5751
|
+
};
|
|
5752
|
+
const onParentBusy = () => {
|
|
5753
|
+
if (!pendingWait) return;
|
|
5754
|
+
debugOpencode(
|
|
5755
|
+
"\u2605 parent resumed after %dms of background wait",
|
|
5756
|
+
pendingWait.elapsedMs()
|
|
5757
|
+
);
|
|
5758
|
+
endWait();
|
|
5759
|
+
emitTasks(liveChildren(), false);
|
|
5760
|
+
};
|
|
5761
|
+
const onInjectedResult = (childId) => {
|
|
5762
|
+
if (!children.has(childId)) return;
|
|
5763
|
+
setChildLive(childId, false);
|
|
5764
|
+
onParentBusy();
|
|
5765
|
+
};
|
|
4843
5766
|
try {
|
|
4844
|
-
const interactiveApproval = isInteractiveApproval(request.options);
|
|
5767
|
+
const interactiveApproval = !request.options.fullAccess && isInteractiveApproval(request.options);
|
|
4845
5768
|
let forkedSession = null;
|
|
4846
5769
|
if (request.run.forkSessionId) {
|
|
4847
5770
|
forkedSession = await fetchJson(
|
|
@@ -4942,6 +5865,12 @@ var OpenCodeAgentAdapter = class {
|
|
|
4942
5865
|
const info = properties?.info;
|
|
4943
5866
|
if (info && typeof info.id === "string" && typeof info.parentID === "string" && runSessionIds.has(info.parentID)) {
|
|
4944
5867
|
runSessionIds.add(info.id);
|
|
5868
|
+
if (trackChildren && info.parentID === sessionId) {
|
|
5869
|
+
registerChild(
|
|
5870
|
+
info.id,
|
|
5871
|
+
typeof info.title === "string" ? info.title : void 0
|
|
5872
|
+
);
|
|
5873
|
+
}
|
|
4945
5874
|
}
|
|
4946
5875
|
}
|
|
4947
5876
|
if (eventType === "message.updated") {
|
|
@@ -4982,6 +5911,32 @@ var OpenCodeAgentAdapter = class {
|
|
|
4982
5911
|
}
|
|
4983
5912
|
}
|
|
4984
5913
|
}
|
|
5914
|
+
if (eventType === "question.asked") {
|
|
5915
|
+
const properties = payload.properties;
|
|
5916
|
+
if (properties && typeof properties.id === "string" && typeof properties.sessionID === "string" && await resolveRunSession(properties.sessionID)) {
|
|
5917
|
+
const questions = normalizeUserQuestions("open-code", properties);
|
|
5918
|
+
const response = hasInteractiveQuestions(request.options) ? await sink.requestPermission(createNormalizedEvent("permission.requested", {
|
|
5919
|
+
provider: request.provider,
|
|
5920
|
+
runId: request.runId,
|
|
5921
|
+
raw
|
|
5922
|
+
}, {
|
|
5923
|
+
requestId: properties.id,
|
|
5924
|
+
kind: "question",
|
|
5925
|
+
toolName: "question",
|
|
5926
|
+
title: "Your input is needed",
|
|
5927
|
+
input: properties,
|
|
5928
|
+
questions,
|
|
5929
|
+
canRemember: false
|
|
5930
|
+
})) : void 0;
|
|
5931
|
+
const allowed = response?.decision === "allow";
|
|
5932
|
+
await fetchJson(`${runtime.baseUrl}/question/${encodeURIComponent(properties.id)}/${allowed ? "reply" : "reject"}`, {
|
|
5933
|
+
method: "POST",
|
|
5934
|
+
headers: { "content-type": "application/json", ...runtime.previewHeaders },
|
|
5935
|
+
body: JSON.stringify(allowed ? { answers: questionReply("open-code", properties, response.answers ?? []) } : {})
|
|
5936
|
+
});
|
|
5937
|
+
}
|
|
5938
|
+
continue;
|
|
5939
|
+
}
|
|
4985
5940
|
if (eventType === "permission.asked") {
|
|
4986
5941
|
const properties = payload.properties;
|
|
4987
5942
|
if (properties && typeof properties.sessionID === "string" && await resolveRunSession(properties.sessionID)) {
|
|
@@ -5024,28 +5979,35 @@ var OpenCodeAgentAdapter = class {
|
|
|
5024
5979
|
const errMsg = typeof errData?.data?.message === "string" ? errData.data.message : typeof errData?.message === "string" ? errData.message : "OpenCode session error";
|
|
5025
5980
|
sessionErrorFromSse = new Error(errMsg);
|
|
5026
5981
|
}
|
|
5982
|
+
resolveSessionTerminal();
|
|
5027
5983
|
} else {
|
|
5028
|
-
|
|
5984
|
+
await onParentIdle();
|
|
5029
5985
|
}
|
|
5030
5986
|
debugOpencode(
|
|
5031
5987
|
"\u2605 %s for session=%s",
|
|
5032
5988
|
payloadRecord.type,
|
|
5033
5989
|
sessionId
|
|
5034
5990
|
);
|
|
5035
|
-
|
|
5991
|
+
} else if (trackChildren && payloadRecord.type === "session.idle") {
|
|
5992
|
+
setChildLive(eventSessionId, false);
|
|
5036
5993
|
}
|
|
5037
5994
|
}
|
|
5038
5995
|
if (payloadRecord?.type === "session.status") {
|
|
5039
5996
|
const properties = payloadRecord.properties;
|
|
5040
5997
|
const status = properties?.status;
|
|
5041
5998
|
const eventSessionId = typeof properties?.sessionID === "string" ? properties.sessionID : void 0;
|
|
5042
|
-
if (
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5999
|
+
if (!eventSessionId || eventSessionId === sessionId) {
|
|
6000
|
+
if (status?.type === "idle") {
|
|
6001
|
+
debugOpencode(
|
|
6002
|
+
"\u2605 session.status{idle} for session=%s",
|
|
6003
|
+
sessionId
|
|
6004
|
+
);
|
|
6005
|
+
await onParentIdle();
|
|
6006
|
+
} else if (status?.type === "busy" || status?.type === "retry") {
|
|
6007
|
+
onParentBusy();
|
|
6008
|
+
}
|
|
6009
|
+
} else if (trackChildren) {
|
|
6010
|
+
setChildLive(eventSessionId, status?.type !== "idle");
|
|
5049
6011
|
}
|
|
5050
6012
|
}
|
|
5051
6013
|
if (payloadRecord?.type === "message.part.updated") {
|
|
@@ -5054,6 +6016,22 @@ var OpenCodeAgentAdapter = class {
|
|
|
5054
6016
|
if (part && typeof part.id === "string" && typeof part.type === "string") {
|
|
5055
6017
|
partTypeById.set(part.id, part.type);
|
|
5056
6018
|
}
|
|
6019
|
+
if (trackChildren && part?.type === "tool" && part.tool === "task" && part.sessionID === sessionId) {
|
|
6020
|
+
const state = part.state;
|
|
6021
|
+
const metadata = state?.metadata;
|
|
6022
|
+
const childId = metadata?.sessionId ?? metadata?.jobId;
|
|
6023
|
+
if (metadata?.background === true && typeof childId === "string") {
|
|
6024
|
+
registerChild(
|
|
6025
|
+
childId,
|
|
6026
|
+
typeof state?.title === "string" ? state.title : void 0,
|
|
6027
|
+
true
|
|
6028
|
+
);
|
|
6029
|
+
}
|
|
6030
|
+
}
|
|
6031
|
+
if (trackChildren && part?.type === "text" && part.synthetic === true && part.sessionID === sessionId && typeof part.text === "string") {
|
|
6032
|
+
const childId = injectedTaskResultChild(part.text);
|
|
6033
|
+
if (childId) onInjectedResult(childId);
|
|
6034
|
+
}
|
|
5057
6035
|
}
|
|
5058
6036
|
if (payloadRecord?.type === "message.part.delta") {
|
|
5059
6037
|
const properties = payloadRecord.properties;
|
|
@@ -5064,6 +6042,9 @@ var OpenCodeAgentAdapter = class {
|
|
|
5064
6042
|
if (isForeignSession) {
|
|
5065
6043
|
continue;
|
|
5066
6044
|
}
|
|
6045
|
+
if (eventMessageId !== void 0 && announcedUserMessageIds.has(eventMessageId)) {
|
|
6046
|
+
continue;
|
|
6047
|
+
}
|
|
5067
6048
|
const delta = typeof properties?.delta === "string" ? properties.delta : "";
|
|
5068
6049
|
const field = typeof properties?.field === "string" ? properties.field : void 0;
|
|
5069
6050
|
const partType = eventPartId ? partTypeById.get(eventPartId) : void 0;
|
|
@@ -5129,6 +6110,12 @@ var OpenCodeAgentAdapter = class {
|
|
|
5129
6110
|
})
|
|
5130
6111
|
);
|
|
5131
6112
|
const agentSlug = openCodeAgentSlug(request.run.reasoning);
|
|
6113
|
+
if (request.run.goal) throw new Error("Native goals are not supported by OpenCode.");
|
|
6114
|
+
if (request.run.mode) {
|
|
6115
|
+
const agents = await fetchJson(`${runtime.baseUrl}/agent`, { headers: runtime.previewHeaders });
|
|
6116
|
+
const name = request.run.mode === "plan" ? "plan" : "build";
|
|
6117
|
+
if (!agents.some((agent) => agent.name === name)) throw new Error(`This OpenCode installation does not expose the ${name} agent.`);
|
|
6118
|
+
}
|
|
5132
6119
|
const dispatchPrompt = async (parts) => {
|
|
5133
6120
|
const body = JSON.stringify({
|
|
5134
6121
|
...request.run.model ? { model: toOpenCodeModel(request.run.model) } : {},
|
|
@@ -5144,7 +6131,8 @@ var OpenCodeAgentAdapter = class {
|
|
|
5144
6131
|
// instead. This per-message field stays as a per-run
|
|
5145
6132
|
// override path that's effective for codex/GPT/Gemini.
|
|
5146
6133
|
...request.run.systemPrompt ? { system: request.run.systemPrompt } : {},
|
|
5147
|
-
agent: agentSlug,
|
|
6134
|
+
...request.options.configuration === "native" ? request.run.reasoning ? { variant: request.run.reasoning } : {} : { agent: agentSlug },
|
|
6135
|
+
...request.run.mode ? { agent: request.run.mode === "plan" ? "plan" : "build" } : {},
|
|
5148
6136
|
parts
|
|
5149
6137
|
});
|
|
5150
6138
|
const url = `${runtime.baseUrl}/session/${sessionId}/prompt_async`;
|
|
@@ -5207,13 +6195,22 @@ var OpenCodeAgentAdapter = class {
|
|
|
5207
6195
|
const SSE_POLL_INTERVAL_MS = 5e3;
|
|
5208
6196
|
lastSseActivityAt = Date.now();
|
|
5209
6197
|
let sseSilent = false;
|
|
5210
|
-
while (!sessionIdleFromSse && !sessionErrorFromSse && !sessionAbortedFromSse && !userAbortRequested && !dispatchError) {
|
|
6198
|
+
while (!sessionIdleFromSse && !sessionErrorFromSse && !sessionAbortedFromSse && !userAbortRequested && !dispatchError && !expiry) {
|
|
5211
6199
|
const silence = Date.now() - lastSseActivityAt;
|
|
5212
6200
|
if (silence > SSE_SILENCE_THRESHOLD_MS) {
|
|
6201
|
+
if (pendingWait && sawParentIdle) {
|
|
6202
|
+
debugOpencode(
|
|
6203
|
+
"SSE went silent (%dms) during background wait; settling",
|
|
6204
|
+
silence
|
|
6205
|
+
);
|
|
6206
|
+
expiry = "transport";
|
|
6207
|
+
break;
|
|
6208
|
+
}
|
|
5213
6209
|
sseSilent = true;
|
|
5214
6210
|
debugOpencode("SSE went silent (%dms) \u2014 giving up", silence);
|
|
5215
6211
|
break;
|
|
5216
6212
|
}
|
|
6213
|
+
if (pendingWait && liveChildren().length > 0) await reconcileChildren();
|
|
5217
6214
|
await Promise.race([
|
|
5218
6215
|
sessionTerminal,
|
|
5219
6216
|
new Promise(
|
|
@@ -5223,6 +6220,16 @@ var OpenCodeAgentAdapter = class {
|
|
|
5223
6220
|
}
|
|
5224
6221
|
sseAbort.abort();
|
|
5225
6222
|
await sseTask;
|
|
6223
|
+
if (expiry === "ceiling" || (sessionErrorFromSse || dispatchError) && liveChildren().length > 0) {
|
|
6224
|
+
debugOpencode(
|
|
6225
|
+
"\u2605 run over (%s) with %d subagent(s) live; aborting them",
|
|
6226
|
+
expiry ?? "failure",
|
|
6227
|
+
liveChildren().length
|
|
6228
|
+
);
|
|
6229
|
+
await postAbort(sessionId);
|
|
6230
|
+
}
|
|
6231
|
+
endWait();
|
|
6232
|
+
emitTasks([], false);
|
|
5226
6233
|
if (userAbortRequested || sessionAbortedFromSse) {
|
|
5227
6234
|
debugOpencode(
|
|
5228
6235
|
"\u2605 run.cancelled (%dms since execute start)",
|
|
@@ -5236,15 +6243,15 @@ var OpenCodeAgentAdapter = class {
|
|
|
5236
6243
|
sink.fail(sessionErrorFromSse);
|
|
5237
6244
|
} else if (dispatchError) {
|
|
5238
6245
|
sink.fail(dispatchError);
|
|
5239
|
-
} else if (sessionIdleFromSse) {
|
|
6246
|
+
} else if (sessionIdleFromSse || expiry) {
|
|
5240
6247
|
debugOpencode(
|
|
5241
6248
|
"\u2605 run.completed (%dms since execute start) chars=%d",
|
|
5242
6249
|
Date.now() - executeStartedAt,
|
|
5243
6250
|
streamedTextFromSse.length
|
|
5244
6251
|
);
|
|
5245
6252
|
let lastAssistantText = "";
|
|
5246
|
-
for (const [messageId,
|
|
5247
|
-
lastAssistantText =
|
|
6253
|
+
for (const [messageId, text2] of assistantTextByMessageId) {
|
|
6254
|
+
lastAssistantText = text2;
|
|
5248
6255
|
if (!announcedAssistantCompletions.has(messageId)) {
|
|
5249
6256
|
announcedAssistantCompletions.add(messageId);
|
|
5250
6257
|
sink.emitEvent(
|
|
@@ -5254,7 +6261,7 @@ var OpenCodeAgentAdapter = class {
|
|
|
5254
6261
|
provider: request.provider,
|
|
5255
6262
|
runId: request.runId
|
|
5256
6263
|
},
|
|
5257
|
-
{ text }
|
|
6264
|
+
{ text: text2 }
|
|
5258
6265
|
)
|
|
5259
6266
|
);
|
|
5260
6267
|
}
|
|
@@ -5281,6 +6288,7 @@ var OpenCodeAgentAdapter = class {
|
|
|
5281
6288
|
sink.fail(new Error("opencode run ended without a terminal signal"));
|
|
5282
6289
|
}
|
|
5283
6290
|
} finally {
|
|
6291
|
+
endWait();
|
|
5284
6292
|
sseAbort.abort();
|
|
5285
6293
|
if (sseTask) {
|
|
5286
6294
|
await sseTask.catch(() => void 0);
|
|
@@ -5423,6 +6431,18 @@ function createAdapter(provider) {
|
|
|
5423
6431
|
}
|
|
5424
6432
|
}
|
|
5425
6433
|
function prepareAgentOptions(_provider, options) {
|
|
6434
|
+
if (options.stateDirectory !== void 0) {
|
|
6435
|
+
if (options.sandbox) throw new Error("stateDirectory is only supported for host execution.");
|
|
6436
|
+
if (!path11.isAbsolute(options.stateDirectory)) throw new Error("stateDirectory must be an absolute path.");
|
|
6437
|
+
}
|
|
6438
|
+
if (options.sandbox && options.processGroup !== void 0) throw new Error("processGroup is only supported for host execution.");
|
|
6439
|
+
resolveBackgroundTaskTimeoutMs(options.backgroundTaskTimeoutMs);
|
|
6440
|
+
if (options.configuration === "native") {
|
|
6441
|
+
if (options.sandbox) throw new Error("Native configuration is only supported for host execution.");
|
|
6442
|
+
if (options.mcps?.length || options.skills?.length || options.subAgents?.length || options.commands?.length || options.enableRtk) {
|
|
6443
|
+
throw new Error("Native configuration uses the harness's own skills, MCPs, commands, and hooks.");
|
|
6444
|
+
}
|
|
6445
|
+
}
|
|
5426
6446
|
return options;
|
|
5427
6447
|
}
|
|
5428
6448
|
var AgentRunController = class {
|
|
@@ -5552,12 +6572,15 @@ var AgentRunController = class {
|
|
|
5552
6572
|
`Permission request ${response.requestId} is not pending for this run.`
|
|
5553
6573
|
);
|
|
5554
6574
|
}
|
|
6575
|
+
const answers = pending.event.kind === "question" && response.decision === "allow" ? validateUserAnswers(pending.event.questions, response.answers) : void 0;
|
|
6576
|
+
if (response.answers && !answers) throw new Error("Answers are only accepted for an allowed question request");
|
|
5555
6577
|
this.pendingPermissions.delete(response.requestId);
|
|
5556
6578
|
const remember = pending.event.canRemember ? response.remember : void 0;
|
|
5557
6579
|
const resolvedResponse = {
|
|
5558
6580
|
requestId: response.requestId,
|
|
5559
6581
|
decision: response.decision,
|
|
5560
|
-
...remember !== void 0 ? { remember } : {}
|
|
6582
|
+
...remember !== void 0 ? { remember } : {},
|
|
6583
|
+
...answers ? { answers } : {}
|
|
5561
6584
|
};
|
|
5562
6585
|
this.pushEvent(
|
|
5563
6586
|
createNormalizedEvent(
|
|
@@ -5569,7 +6592,8 @@ var AgentRunController = class {
|
|
|
5569
6592
|
{
|
|
5570
6593
|
requestId: response.requestId,
|
|
5571
6594
|
decision: response.decision,
|
|
5572
|
-
...remember !== void 0 ? { remember } : {}
|
|
6595
|
+
...remember !== void 0 ? { remember } : {},
|
|
6596
|
+
...answers ? { answers } : {}
|
|
5573
6597
|
}
|
|
5574
6598
|
)
|
|
5575
6599
|
);
|
|
@@ -5666,6 +6690,10 @@ var AgentRunController = class {
|
|
|
5666
6690
|
if (this.settled) {
|
|
5667
6691
|
return;
|
|
5668
6692
|
}
|
|
6693
|
+
if (this.abortRequested) {
|
|
6694
|
+
this.cancel();
|
|
6695
|
+
return;
|
|
6696
|
+
}
|
|
5669
6697
|
const normalizedError = asError(error);
|
|
5670
6698
|
this.clearPendingPermissions(normalizedError);
|
|
5671
6699
|
this.emitEvent(
|
|
@@ -5700,6 +6728,7 @@ var AgentRunController = class {
|
|
|
5700
6728
|
}
|
|
5701
6729
|
async abort() {
|
|
5702
6730
|
this.abortRequested = true;
|
|
6731
|
+
this.clearPendingPermissions(new Error("Agent run cancelled"));
|
|
5703
6732
|
await this.abortHandler();
|
|
5704
6733
|
}
|
|
5705
6734
|
rawEvents() {
|
|
@@ -5812,7 +6841,7 @@ var Agent = class {
|
|
|
5812
6841
|
if (runConfig.forkAtMessageId && !runConfig.forkSessionId) {
|
|
5813
6842
|
throw new Error("AgentRunConfig.forkAtMessageId requires forkSessionId.");
|
|
5814
6843
|
}
|
|
5815
|
-
const runId = runConfig.runId ??
|
|
6844
|
+
const runId = runConfig.runId ?? randomUUID3();
|
|
5816
6845
|
const streamCalledAt = Date.now();
|
|
5817
6846
|
debugAgent("stream() provider=%s runId=%s", this.provider, runId);
|
|
5818
6847
|
const run = new AgentRunController(this.provider, runId);
|
|
@@ -5880,8 +6909,33 @@ var Agent = class {
|
|
|
5880
6909
|
}
|
|
5881
6910
|
};
|
|
5882
6911
|
|
|
6912
|
+
// src/agents/commands.ts
|
|
6913
|
+
function harnessCapabilities(provider) {
|
|
6914
|
+
return { commands: provider === "open-code" ? ["plan", "agent"] : ["plan", "agent", "goal"], planning: provider === "codex" ? "explicit" : "agent-directed", questions: true, fullAccess: true };
|
|
6915
|
+
}
|
|
6916
|
+
function resolveHarnessCommand(provider, input) {
|
|
6917
|
+
const first = typeof input === "string" ? input : input.find((part) => part.type === "text")?.text;
|
|
6918
|
+
const match = first?.match(/^\s*\/(plan|agent|goal)(?:\s+([\s\S]*))?$/);
|
|
6919
|
+
if (!match) return { input };
|
|
6920
|
+
const command = match[1];
|
|
6921
|
+
if (!harnessCapabilities(provider).commands.includes(command)) throw new Error(`/${command} is not supported by ${provider}.`);
|
|
6922
|
+
const body = match[2]?.trim() ?? "";
|
|
6923
|
+
if (command === "goal" && (!body || body.length > 4e3)) throw new Error("/goal requires an objective of 1\u20134000 characters.");
|
|
6924
|
+
if (command === "goal" && provider === "claude-code") return { input, goal: body };
|
|
6925
|
+
const text2 = body || (command === "plan" ? "Plan the requested work." : "Continue with implementation.");
|
|
6926
|
+
let replaced = false;
|
|
6927
|
+
const cleaned = typeof input === "string" ? text2 : input.map((part) => {
|
|
6928
|
+
if (part.type !== "text" || replaced) return part;
|
|
6929
|
+
replaced = true;
|
|
6930
|
+
return { ...part, text: text2 };
|
|
6931
|
+
});
|
|
6932
|
+
return command === "goal" ? { input: cleaned, goal: body } : { input: cleaned, mode: command === "plan" ? "plan" : "default" };
|
|
6933
|
+
}
|
|
6934
|
+
|
|
5883
6935
|
export {
|
|
5884
6936
|
agentboxRoot,
|
|
5885
6937
|
getAgentLayout,
|
|
5886
|
-
Agent
|
|
6938
|
+
Agent,
|
|
6939
|
+
harnessCapabilities,
|
|
6940
|
+
resolveHarnessCommand
|
|
5887
6941
|
};
|