@agent-compose/sdk 0.5.5 → 0.5.7
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 +4 -4
- package/dist/agent/agent-loop-contract.test.d.ts +1 -0
- package/dist/agent/agent-loop.d.ts +1 -1
- package/dist/client.d.ts +65 -23
- package/dist/index.d.ts +4 -2
- package/dist/index.js +389 -109
- package/dist/runtimes/_cli-agent.d.ts +25 -8
- package/dist/runtimes/amp.d.ts +7 -6
- package/dist/runtimes/claude.d.ts +9 -1
- package/dist/runtimes/cli-agent.test.d.ts +9 -0
- package/dist/runtimes/codex.d.ts +6 -5
- package/dist/runtimes/openai-desktop.js +387 -109
- package/dist/sandbox-errors.d.ts +49 -0
- package/dist/sandbox.d.ts +68 -13
- package/dist/step-invocation/protocol.d.ts +6 -0
- package/dist/types/sandbox-environment.d.ts +1 -10
- package/dist/types/sandbox.d.ts +27 -3
- package/dist/types/workflow-metadata.d.ts +88 -23
- package/dist/types/workflow.d.ts +38 -10
- package/dist/utils/bundler.d.ts +38 -9
- package/dist/workflow-steps/workflow.d.ts +1 -3
- package/package.json +2 -2
- package/src/agent/agent-loop.ts +56 -7
- package/src/client.ts +144 -25
- package/src/index.ts +4 -2
- package/src/runtimes/_cli-agent.ts +75 -42
- package/src/runtimes/amp.ts +11 -6
- package/src/runtimes/claude.ts +66 -8
- package/src/runtimes/codex.ts +10 -5
- package/src/sandbox-errors.ts +53 -0
- package/src/sandbox.ts +378 -56
- package/src/step-invocation/invoker.ts +66 -8
- package/src/step-invocation/protocol.ts +9 -0
- package/src/step-invocation/server.ts +27 -4
- package/src/types/sandbox-environment.ts +1 -11
- package/src/types/sandbox.ts +28 -3
- package/src/types/workflow-metadata.ts +97 -26
- package/src/types/workflow.ts +38 -11
- package/src/utils/bundler.ts +43 -13
- package/src/workflow-steps/workflow.ts +1 -3
|
@@ -542,10 +542,12 @@ function extractMetadata(source) {
|
|
|
542
542
|
out.snapshots = Object.freeze({ ...source.snapshots });
|
|
543
543
|
if (source.processors !== undefined)
|
|
544
544
|
out.processors = Object.freeze([...source.processors]);
|
|
545
|
-
if (source.
|
|
546
|
-
out.
|
|
547
|
-
if (source.
|
|
548
|
-
out.
|
|
545
|
+
if (source.connectors !== undefined)
|
|
546
|
+
out.connectors = freezeMetadataValue(source.connectors);
|
|
547
|
+
if (source.connectorOperation !== undefined)
|
|
548
|
+
out.connectorOperation = Object.freeze({ ...source.connectorOperation });
|
|
549
|
+
if (source.invokePolicy !== undefined)
|
|
550
|
+
out.invokePolicy = freezeMetadataValue(source.invokePolicy);
|
|
549
551
|
return Object.freeze(out);
|
|
550
552
|
}
|
|
551
553
|
function freezeMetadataValue(value) {
|
|
@@ -649,7 +651,6 @@ function defineSandboxEnvironment(env) {
|
|
|
649
651
|
return defineWorkflow({
|
|
650
652
|
...env.description !== undefined ? { description: env.description } : {},
|
|
651
653
|
snapshots: env.snapshots ?? { saveLatest: true },
|
|
652
|
-
memory: env.memory ?? false,
|
|
653
654
|
run: async (_ctx, sandbox) => env.setup(sandbox)
|
|
654
655
|
});
|
|
655
656
|
}
|
|
@@ -800,15 +801,17 @@ class AgentComposeClient {
|
|
|
800
801
|
invoke(name, input, opts) {
|
|
801
802
|
const parentRunId = opts?.parentRunId === undefined ? detectAmbientParentRunId() : opts.parentRunId;
|
|
802
803
|
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
804
|
+
const headers = {};
|
|
805
|
+
if (opts?.idempotencyKey)
|
|
806
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
803
807
|
return this.fetch(templatePath(factorySlug, name, "invoke"), {
|
|
804
808
|
method: "POST",
|
|
809
|
+
...opts?.idempotencyKey ? { headers } : {},
|
|
805
810
|
body: {
|
|
806
811
|
input,
|
|
807
812
|
...opts?.snapshots !== undefined ? { snapshots: opts.snapshots } : {},
|
|
808
813
|
...opts?.networkPolicy !== undefined ? { networkPolicy: opts.networkPolicy } : {},
|
|
809
814
|
...opts?.placeholders !== undefined ? { placeholders: opts.placeholders } : {},
|
|
810
|
-
...opts?.memory !== undefined ? { memory: opts.memory } : {},
|
|
811
|
-
...opts?.postRunHooks !== undefined ? { postRunHooks: opts.postRunHooks } : {},
|
|
812
815
|
...parentRunId ? { parentRunId } : {},
|
|
813
816
|
...opts?.agentId ? { agentId: opts.agentId } : {}
|
|
814
817
|
}
|
|
@@ -933,6 +936,41 @@ class AgentComposeClient {
|
|
|
933
936
|
const body = await this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/events`);
|
|
934
937
|
return body.events;
|
|
935
938
|
}
|
|
939
|
+
async listRunArtifacts(runId) {
|
|
940
|
+
const body = await this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/artifacts`);
|
|
941
|
+
return body.artifacts.map((a) => ({
|
|
942
|
+
path: a.path,
|
|
943
|
+
factorySlug: a.factory_slug,
|
|
944
|
+
sizeBytes: a.size_bytes,
|
|
945
|
+
lastWriteAt: a.last_write_at,
|
|
946
|
+
preview: a.preview
|
|
947
|
+
}));
|
|
948
|
+
}
|
|
949
|
+
async putFactoryFile(path, content, opts) {
|
|
950
|
+
const factorySlug = opts?.factorySlug ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined) ?? DEFAULT_FACTORY;
|
|
951
|
+
const runToken = typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_RUN_TOKEN : undefined;
|
|
952
|
+
const wire = await this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/content?path=${encodeURIComponent(path)}`, {
|
|
953
|
+
method: "PUT",
|
|
954
|
+
body: content,
|
|
955
|
+
headers: {
|
|
956
|
+
"content-type": opts?.contentType ?? "text/plain; charset=utf-8",
|
|
957
|
+
...runToken ? { "x-run-token": runToken } : {}
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
return {
|
|
961
|
+
path: wire.path,
|
|
962
|
+
contentHash: wire.content_hash,
|
|
963
|
+
sizeBytes: wire.size_bytes,
|
|
964
|
+
created: wire.created
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
async getFactoryFile(path, opts) {
|
|
968
|
+
const factorySlug = opts?.factorySlug ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined) ?? DEFAULT_FACTORY;
|
|
969
|
+
const q2 = new URLSearchParams({ path });
|
|
970
|
+
if (opts?.revision !== undefined)
|
|
971
|
+
q2.set("revision", String(opts.revision));
|
|
972
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/content?${q2}`, { responseType: "text" });
|
|
973
|
+
}
|
|
936
974
|
async listFactoryEvents(opts) {
|
|
937
975
|
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
938
976
|
const q2 = new URLSearchParams;
|
|
@@ -1068,7 +1106,7 @@ function workflowPlan(steps) {
|
|
|
1068
1106
|
}
|
|
1069
1107
|
|
|
1070
1108
|
// src/utils/bundler.ts
|
|
1071
|
-
var BUNDLER_VERSION =
|
|
1109
|
+
var BUNDLER_VERSION = 2;
|
|
1072
1110
|
|
|
1073
1111
|
class WorkflowSourceValidationError extends Error {
|
|
1074
1112
|
constructor(message) {
|
|
@@ -1184,8 +1222,9 @@ async function bundleWorkflow(workflowPath, overrides) {
|
|
|
1184
1222
|
...inputSchema !== undefined ? { inputSchema } : {},
|
|
1185
1223
|
...outputSchema !== undefined ? { outputSchema } : {},
|
|
1186
1224
|
...metadata.snapshots !== undefined ? { snapshots: metadata.snapshots } : {},
|
|
1187
|
-
...metadata.
|
|
1188
|
-
...metadata.
|
|
1225
|
+
...metadata.connectors !== undefined ? { connectors: metadata.connectors } : {},
|
|
1226
|
+
...metadata.connectorOperation !== undefined ? { connectorOperation: metadata.connectorOperation } : {},
|
|
1227
|
+
...metadata.invokePolicy !== undefined ? { invokePolicy: metadata.invokePolicy } : {}
|
|
1189
1228
|
};
|
|
1190
1229
|
}
|
|
1191
1230
|
function extractIOSchema(zodSchema) {
|
|
@@ -1812,7 +1851,7 @@ async function runControlPoller(opts) {
|
|
|
1812
1851
|
}
|
|
1813
1852
|
|
|
1814
1853
|
// src/agent/agent-loop.ts
|
|
1815
|
-
var DEFAULT_CLAUDE_MODEL = "claude-
|
|
1854
|
+
var DEFAULT_CLAUDE_MODEL = "claude-fable-5";
|
|
1816
1855
|
var SAME_BLOCKER_ITERATIONS = 3;
|
|
1817
1856
|
var STALL_ITERATIONS = 3;
|
|
1818
1857
|
var MESSAGE_PREVIEW_CHARS = 400;
|
|
@@ -1871,8 +1910,9 @@ async function agentLoop(opts) {
|
|
|
1871
1910
|
const label = opts.label ?? "agent";
|
|
1872
1911
|
const logLabel = opts.label ?? "[Agent Loop]";
|
|
1873
1912
|
const startedAt = Date.now();
|
|
1874
|
-
const turnsPerIteration = opts.turnsPerIteration
|
|
1875
|
-
const maxIterations = opts.maxIterations ?? 8;
|
|
1913
|
+
const turnsPerIteration = opts.turnsPerIteration;
|
|
1914
|
+
const maxIterations = opts.maxIterations ?? (turnsPerIteration === undefined ? 1 : 8);
|
|
1915
|
+
let schemaRetriesLeft = 10;
|
|
1876
1916
|
const processors = opts.processors ?? [];
|
|
1877
1917
|
const requestContext = opts.requestContext ?? RequestContext.fromReserved({
|
|
1878
1918
|
teamId: "",
|
|
@@ -1893,7 +1933,7 @@ async function agentLoop(opts) {
|
|
|
1893
1933
|
if (!opts.runtime)
|
|
1894
1934
|
throw new Error("agentLoop: opts.runtime is required");
|
|
1895
1935
|
const client = opts.runtime({
|
|
1896
|
-
maxTurns: turnsPerIteration,
|
|
1936
|
+
...turnsPerIteration !== undefined ? { maxTurns: turnsPerIteration } : {},
|
|
1897
1937
|
allowedTools: opts.allowedTools ?? DEFAULT_ALLOWED_TOOLS,
|
|
1898
1938
|
label: logLabel,
|
|
1899
1939
|
cwd: opts.cwd,
|
|
@@ -2002,6 +2042,15 @@ async function agentLoop(opts) {
|
|
|
2002
2042
|
}
|
|
2003
2043
|
const procCtx = buildProcCtx(iteration + 1);
|
|
2004
2044
|
let initialPrompt = opts.buildPrompt(lastStatus, iteration);
|
|
2045
|
+
if (lastResponseValidationError) {
|
|
2046
|
+
initialPrompt = `${initialPrompt}
|
|
2047
|
+
|
|
2048
|
+
[response contract violation — fix and re-emit]
|
|
2049
|
+
Your previous <response> failed schema validation with these errors:
|
|
2050
|
+
${lastResponseValidationError.slice(0, 2000)}
|
|
2051
|
+
Re-emit the COMPLETE corrected <response> JSON now: every required field present, correctly named and typed (no omissions, no renames).`;
|
|
2052
|
+
lastResponseValidationError = "";
|
|
2053
|
+
}
|
|
2005
2054
|
if (steerDecision) {
|
|
2006
2055
|
const who = steerDecision.actor ? ` from ${steerDecision.actor}` : "";
|
|
2007
2056
|
initialPrompt = `${initialPrompt}
|
|
@@ -2014,12 +2063,12 @@ async function agentLoop(opts) {
|
|
|
2014
2063
|
throw new Error(`${logLabel} input ${inputVerdict.kind === "deny" ? "rejected" : "aborted"}: ${inputVerdict.reason}`);
|
|
2015
2064
|
}
|
|
2016
2065
|
const prompt = inputVerdict.value;
|
|
2017
|
-
process.stdout.write(`${logLabel} iteration ${iteration + 1}/${maxIterations} · ${turnsPerIteration} turns
|
|
2066
|
+
process.stdout.write(`${logLabel} iteration ${iteration + 1}/${maxIterations} · ${turnsPerIteration !== undefined ? `${turnsPerIteration} turns` : "harness-decided turns"}
|
|
2018
2067
|
`);
|
|
2019
2068
|
let responseText = "";
|
|
2020
2069
|
for await (const rawMsg of client.sendMessage({
|
|
2021
2070
|
prompt,
|
|
2022
|
-
sessionId:
|
|
2071
|
+
sessionId: lastSessionId ?? undefined,
|
|
2023
2072
|
iteration: iteration + 1,
|
|
2024
2073
|
...opts.inbox ? { inboxStream: opts.inbox } : {}
|
|
2025
2074
|
})) {
|
|
@@ -2078,6 +2127,8 @@ async function agentLoop(opts) {
|
|
|
2078
2127
|
`);
|
|
2079
2128
|
status = { ...status, exit_signal: false, blockers: ["No <response> block found — emit a <response> block with the required JSON fields before setting exit_signal: true"] };
|
|
2080
2129
|
opts.onIteration?.(iteration + 1, status);
|
|
2130
|
+
if (schemaRetriesLeft-- > 0)
|
|
2131
|
+
iteration--;
|
|
2081
2132
|
continue;
|
|
2082
2133
|
}
|
|
2083
2134
|
const parsed = validateResponse({ ...status, ...rawResponse });
|
|
@@ -2087,6 +2138,8 @@ raw: ${JSON.stringify(rawResponse).slice(0, 400)}
|
|
|
2087
2138
|
`);
|
|
2088
2139
|
status = { ...status, exit_signal: false, blockers: [`<response> schema validation failed: ${parsed.error.message}`] };
|
|
2089
2140
|
opts.onIteration?.(iteration + 1, status);
|
|
2141
|
+
if (schemaRetriesLeft-- > 0)
|
|
2142
|
+
iteration--;
|
|
2090
2143
|
continue;
|
|
2091
2144
|
}
|
|
2092
2145
|
response = parsed.data;
|
|
@@ -2107,9 +2160,16 @@ raw: ${JSON.stringify(rawResponse).slice(0, 400)}
|
|
|
2107
2160
|
blockerStreak = null;
|
|
2108
2161
|
continue;
|
|
2109
2162
|
}
|
|
2110
|
-
if (!status) {
|
|
2163
|
+
if (!status && rawResponse === null) {
|
|
2111
2164
|
if (++iterationsWithoutStatus >= STALL_ITERATIONS)
|
|
2112
2165
|
throw new Error(`${logLabel} stalled: no <status> block after ${iterationsWithoutStatus} iterations`);
|
|
2166
|
+
if (opts.responseSchema && schemaRetriesLeft-- > 0) {
|
|
2167
|
+
lastResponseValidationError = "no <response> block found — the turn ended with neither a <status> nor a <response> block; emit the complete <response> JSON";
|
|
2168
|
+
process.stdout.write(`${logLabel} no <status>/<response> emitted — corrective re-prompt (${schemaRetriesLeft} retries left)
|
|
2169
|
+
`);
|
|
2170
|
+
iteration--;
|
|
2171
|
+
continue;
|
|
2172
|
+
}
|
|
2113
2173
|
} else {
|
|
2114
2174
|
iterationsWithoutStatus = 0;
|
|
2115
2175
|
}
|
|
@@ -2124,6 +2184,12 @@ raw: ${JSON.stringify(rawResponse).slice(0, 400)}
|
|
|
2124
2184
|
} else {
|
|
2125
2185
|
blockerStreak = null;
|
|
2126
2186
|
}
|
|
2187
|
+
if (opts.responseSchema && rawResponse !== null && lastResponseValidationError !== "" && status?.exit_signal !== false && schemaRetriesLeft-- > 0) {
|
|
2188
|
+
process.stdout.write(`${logLabel} response contract not yet satisfied — corrective re-prompt (${schemaRetriesLeft} retries left)
|
|
2189
|
+
`);
|
|
2190
|
+
iteration--;
|
|
2191
|
+
continue;
|
|
2192
|
+
}
|
|
2127
2193
|
if (iteration + 1 < maxIterations)
|
|
2128
2194
|
process.stdout.write(`${logLabel} continuing to iteration ${iteration + 2}/${maxIterations}
|
|
2129
2195
|
`);
|
|
@@ -2249,14 +2315,34 @@ function translateMessage(message) {
|
|
|
2249
2315
|
numTurns: Number(message.num_turns ?? 0),
|
|
2250
2316
|
timestamp: ts
|
|
2251
2317
|
});
|
|
2252
|
-
if (message.
|
|
2253
|
-
msgs.push({ type: "
|
|
2318
|
+
if (message.subtype === "error_max_turns") {
|
|
2319
|
+
msgs.push({ type: "done", sessionId: String(message.session_id ?? ""), timestamp: ts });
|
|
2320
|
+
} else if (message.is_error || message.subtype === "error_during_execution") {
|
|
2321
|
+
const detail = message.error ?? message.result ?? message.message;
|
|
2322
|
+
const text = detail !== undefined ? formatError(detail) : `${String(message.subtype ?? "unknown_error")} — raw result: ${JSON.stringify({ ...message, usage: undefined }).slice(0, 600)}`;
|
|
2323
|
+
msgs.push({ type: "error", text, timestamp: ts });
|
|
2254
2324
|
} else {
|
|
2255
2325
|
msgs.push({ type: "done", sessionId: String(message.session_id ?? ""), timestamp: ts });
|
|
2256
2326
|
}
|
|
2257
2327
|
}
|
|
2258
2328
|
return msgs;
|
|
2259
2329
|
}
|
|
2330
|
+
var MODEL_TIERS = {
|
|
2331
|
+
fable: "claude-fable-5",
|
|
2332
|
+
opus: "claude-opus-4-8",
|
|
2333
|
+
sonnet: "claude-sonnet-4-6",
|
|
2334
|
+
haiku: "claude-haiku-4-5"
|
|
2335
|
+
};
|
|
2336
|
+
function resolveClaudeModel(model) {
|
|
2337
|
+
if (!model)
|
|
2338
|
+
return DEFAULT_CLAUDE_MODEL;
|
|
2339
|
+
return MODEL_TIERS[model.toLowerCase()] ?? model;
|
|
2340
|
+
}
|
|
2341
|
+
function resolveThinking(model, thinking) {
|
|
2342
|
+
if (model.startsWith("claude-fable") && thinking && thinking.type === "disabled")
|
|
2343
|
+
return;
|
|
2344
|
+
return thinking;
|
|
2345
|
+
}
|
|
2260
2346
|
var DEFAULT_CLAUDE_PATH = "/usr/local/bin/claude";
|
|
2261
2347
|
|
|
2262
2348
|
class ClaudeRunner {
|
|
@@ -2269,7 +2355,7 @@ class ClaudeRunner {
|
|
|
2269
2355
|
this.config = config;
|
|
2270
2356
|
}
|
|
2271
2357
|
get model() {
|
|
2272
|
-
return this.config.model ?? this.options.model
|
|
2358
|
+
return resolveClaudeModel(this.config.model ?? this.options.model);
|
|
2273
2359
|
}
|
|
2274
2360
|
async gateToolCall(call, ctx) {
|
|
2275
2361
|
const verdict = await runProcessorChain(this.options.processors ?? [], (p) => p.processToolCall, call, ctx);
|
|
@@ -2337,14 +2423,15 @@ class ClaudeRunner {
|
|
|
2337
2423
|
tools: this.options.allowedTools,
|
|
2338
2424
|
allowedTools: this.options.allowedTools,
|
|
2339
2425
|
maxTurns: this.options.maxTurns,
|
|
2340
|
-
model: this.
|
|
2426
|
+
model: this.model,
|
|
2341
2427
|
outputFormat: this.options.outputFormat,
|
|
2342
|
-
thinking: this.config.thinking,
|
|
2428
|
+
thinking: resolveThinking(this.model, this.config.thinking),
|
|
2343
2429
|
effort: this.config.effort,
|
|
2344
2430
|
cwd: this.options.cwd,
|
|
2345
2431
|
env: { ...process.env, ...this.config.env ?? {} },
|
|
2346
2432
|
pathToClaudeCodeExecutable: this.config.pathToClaudeCodeExecutable ?? process.env.CLAUDE_CODE_EXECUTABLE ?? DEFAULT_CLAUDE_PATH,
|
|
2347
2433
|
...this.config.claudeMdContent ? { systemPrompt: { type: "preset", preset: "claude_code", append: this.config.claudeMdContent } } : {},
|
|
2434
|
+
skills: this.config.skills ?? "all",
|
|
2348
2435
|
resume: opts.sessionId,
|
|
2349
2436
|
mcpServers: this.config.mcpServers,
|
|
2350
2437
|
permissionMode: "acceptEdits",
|
|
@@ -2364,7 +2451,12 @@ class ClaudeRunner {
|
|
|
2364
2451
|
inboxQueue.close();
|
|
2365
2452
|
}
|
|
2366
2453
|
} catch (err) {
|
|
2367
|
-
|
|
2454
|
+
const text = formatError(err);
|
|
2455
|
+
if (/maximum number of turns/i.test(text)) {
|
|
2456
|
+
yield { type: "done", sessionId: opts.sessionId ?? "", timestamp: now2() };
|
|
2457
|
+
} else {
|
|
2458
|
+
yield { type: "error", text, timestamp: now2() };
|
|
2459
|
+
}
|
|
2368
2460
|
} finally {
|
|
2369
2461
|
inboxQueue?.close();
|
|
2370
2462
|
}
|
|
@@ -2400,45 +2492,61 @@ class CliAgentRunner {
|
|
|
2400
2492
|
get model() {
|
|
2401
2493
|
return this.configModel ?? this.options.model ?? this.spec.defaultModel;
|
|
2402
2494
|
}
|
|
2495
|
+
installed = false;
|
|
2496
|
+
async ensureInstalled() {
|
|
2497
|
+
if (this.installed)
|
|
2498
|
+
return;
|
|
2499
|
+
const probe = await this.sandbox.commands.run(`command -v ${this.spec.bin}`);
|
|
2500
|
+
if (probe.exitCode === 0) {
|
|
2501
|
+
this.installed = true;
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
const res = await this.sandbox.commands.run(this.spec.install, { timeoutMs: 300000 });
|
|
2505
|
+
if (res.exitCode !== 0) {
|
|
2506
|
+
throw new Error(`failed to install ${this.spec.bin}: ${(res.stderr || res.stdout || "").slice(-500)}`);
|
|
2507
|
+
}
|
|
2508
|
+
this.installed = true;
|
|
2509
|
+
}
|
|
2403
2510
|
async* sendMessage(opts) {
|
|
2404
|
-
const promptPath = `/tmp/ac-${this.spec.kind}-${this.options.agentId ?? "agent"}-${opts.iteration ?? 0}.in`;
|
|
2405
|
-
await this.sandbox.files.write(promptPath, this.spec.promptPayload(opts.prompt));
|
|
2406
|
-
const cmd = this.spec.buildCommand({
|
|
2407
|
-
promptPath,
|
|
2408
|
-
sessionId: opts.sessionId,
|
|
2409
|
-
model: this.model,
|
|
2410
|
-
cwd: this.options.cwd
|
|
2411
|
-
});
|
|
2412
|
-
const lines = new AsyncQueue;
|
|
2413
|
-
let buf = "";
|
|
2414
|
-
const onStdout = (data) => {
|
|
2415
|
-
buf += data;
|
|
2416
|
-
let nl;
|
|
2417
|
-
while ((nl = buf.indexOf(`
|
|
2418
|
-
`)) >= 0) {
|
|
2419
|
-
const line = buf.slice(0, nl).trim();
|
|
2420
|
-
buf = buf.slice(nl + 1);
|
|
2421
|
-
if (line)
|
|
2422
|
-
lines.push(line);
|
|
2423
|
-
}
|
|
2424
|
-
};
|
|
2425
|
-
const runPromise = this.sandbox.commands.run(cmd, {
|
|
2426
|
-
...this.options.cwd ? { cwd: this.options.cwd } : {},
|
|
2427
|
-
onStdout
|
|
2428
|
-
}).then((res) => {
|
|
2429
|
-
const tail = buf.trim();
|
|
2430
|
-
if (tail)
|
|
2431
|
-
lines.push(tail);
|
|
2432
|
-
lines.close();
|
|
2433
|
-
return res;
|
|
2434
|
-
}, (err) => {
|
|
2435
|
-
lines.close();
|
|
2436
|
-
throw err;
|
|
2437
|
-
});
|
|
2438
2511
|
yield { type: "init", sessionId: opts.sessionId ?? "", timestamp: now3() };
|
|
2439
2512
|
let sessionId = opts.sessionId;
|
|
2440
2513
|
let sawError = false;
|
|
2441
2514
|
try {
|
|
2515
|
+
await this.ensureInstalled();
|
|
2516
|
+
const promptPath = `/tmp/ac-${this.spec.kind}-${this.options.agentId ?? "agent"}-${opts.iteration ?? 0}.in`;
|
|
2517
|
+
await this.sandbox.files.write(promptPath, this.spec.promptPayload(opts.prompt));
|
|
2518
|
+
const cmd = this.spec.buildCommand({
|
|
2519
|
+
promptPath,
|
|
2520
|
+
sessionId: opts.sessionId,
|
|
2521
|
+
model: this.model,
|
|
2522
|
+
cwd: this.options.cwd
|
|
2523
|
+
});
|
|
2524
|
+
const lines = new AsyncQueue;
|
|
2525
|
+
let buf = "";
|
|
2526
|
+
const onStdout = (data) => {
|
|
2527
|
+
buf += data;
|
|
2528
|
+
let nl;
|
|
2529
|
+
while ((nl = buf.indexOf(`
|
|
2530
|
+
`)) >= 0) {
|
|
2531
|
+
const line = buf.slice(0, nl).trim();
|
|
2532
|
+
buf = buf.slice(nl + 1);
|
|
2533
|
+
if (line)
|
|
2534
|
+
lines.push(line);
|
|
2535
|
+
}
|
|
2536
|
+
};
|
|
2537
|
+
const runPromise = this.sandbox.commands.run(cmd, {
|
|
2538
|
+
...this.options.cwd ? { cwd: this.options.cwd } : {},
|
|
2539
|
+
onStdout
|
|
2540
|
+
}).then((res2) => {
|
|
2541
|
+
const tail = buf.trim();
|
|
2542
|
+
if (tail)
|
|
2543
|
+
lines.push(tail);
|
|
2544
|
+
lines.close();
|
|
2545
|
+
return res2;
|
|
2546
|
+
}, (err) => {
|
|
2547
|
+
lines.close();
|
|
2548
|
+
throw err;
|
|
2549
|
+
});
|
|
2442
2550
|
for await (const line of lines) {
|
|
2443
2551
|
let parsed;
|
|
2444
2552
|
try {
|
|
@@ -2481,6 +2589,8 @@ function now4() {
|
|
|
2481
2589
|
var codexSpec = {
|
|
2482
2590
|
kind: "codex",
|
|
2483
2591
|
authEnv: "CODEX_API_KEY",
|
|
2592
|
+
bin: "codex",
|
|
2593
|
+
install: 'sudo npm install -g @openai/codex && (command -v codex >/dev/null 2>&1 || sudo ln -sf "$(npm prefix -g)/bin/codex" /usr/local/bin/codex)',
|
|
2484
2594
|
promptPayload: (prompt) => prompt,
|
|
2485
2595
|
buildCommand: ({ promptPath, sessionId, model, cwd }) => {
|
|
2486
2596
|
const flags = [
|
|
@@ -2560,6 +2670,8 @@ function blocks(msg) {
|
|
|
2560
2670
|
var ampSpec = {
|
|
2561
2671
|
kind: "amp",
|
|
2562
2672
|
authEnv: "AMP_API_KEY",
|
|
2673
|
+
bin: "amp",
|
|
2674
|
+
install: 'sudo npm install -g @ampcode/cli && (command -v amp >/dev/null 2>&1 || sudo ln -sf "$(npm prefix -g)/bin/amp" /usr/local/bin/amp)',
|
|
2563
2675
|
promptPayload: (prompt) => JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text: prompt }] } }) + `
|
|
2564
2676
|
`,
|
|
2565
2677
|
buildCommand: ({ promptPath, sessionId }) => {
|
|
@@ -2613,9 +2725,25 @@ var amp_default = createAmpRuntime();
|
|
|
2613
2725
|
import { promises as fs2 } from "node:fs";
|
|
2614
2726
|
import { dirname as dirname2 } from "node:path";
|
|
2615
2727
|
import { spawn } from "node:child_process";
|
|
2616
|
-
import { Sandbox } from "e2b";
|
|
2728
|
+
import { Sandbox, SandboxNotFoundError, RateLimitError } from "e2b";
|
|
2617
2729
|
import { Sandbox as Desktop } from "@e2b/desktop";
|
|
2618
2730
|
import pRetry from "p-retry";
|
|
2731
|
+
|
|
2732
|
+
// src/sandbox-errors.ts
|
|
2733
|
+
var SANDBOX_UNAVAILABLE_PREFIX = "[sandbox-unavailable";
|
|
2734
|
+
|
|
2735
|
+
class SandboxUnavailableError extends Error {
|
|
2736
|
+
retryable;
|
|
2737
|
+
sandboxId;
|
|
2738
|
+
constructor(detail, opts) {
|
|
2739
|
+
super(`${SANDBOX_UNAVAILABLE_PREFIX}:${opts.retryable ? "retryable" : "terminal"}] ${detail}`);
|
|
2740
|
+
this.name = "SandboxUnavailableError";
|
|
2741
|
+
this.retryable = opts.retryable;
|
|
2742
|
+
this.sandboxId = opts.sandboxId;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
// src/sandbox.ts
|
|
2619
2747
|
var AGENT_COMPOSE_TAG = process.env.AGENT_COMPOSE_TAG ?? `agent-compose-${process.env.AGENT_COMPOSE_ENV ?? "dev"}`;
|
|
2620
2748
|
var VERCEL_VM_LIFETIME_WINDOW_MS = 6 * 60 * 60 * 1000;
|
|
2621
2749
|
function makeSandboxProvider(sb) {
|
|
@@ -2647,6 +2775,33 @@ function makeSandboxProvider(sb) {
|
|
|
2647
2775
|
kill: () => sb.kill()
|
|
2648
2776
|
};
|
|
2649
2777
|
}
|
|
2778
|
+
function e2bMaxSandboxMs() {
|
|
2779
|
+
return Number(process.env.E2B_MAX_SANDBOX_MS) || 60 * 60 * 1000;
|
|
2780
|
+
}
|
|
2781
|
+
function isTransientSandboxError(error) {
|
|
2782
|
+
if (error instanceof SandboxNotFoundError || error instanceof RateLimitError)
|
|
2783
|
+
return true;
|
|
2784
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
2785
|
+
return /paus|stopp|resum|transition|status(?:\s+code)?[:\s]+5\d\d\b|\b5\d\d\s+(?:internal|bad gateway|service|gateway)|\b429\b|ECONNRESET|ECONNREFUSED|ETIMEDOUT|socket hang up|fetch failed/i.test(message);
|
|
2786
|
+
}
|
|
2787
|
+
function withSandboxRetry(fn, shouldRetry = isTransientSandboxError) {
|
|
2788
|
+
return pRetry(fn, { retries: 4, minTimeout: 400, factor: 2, shouldRetry });
|
|
2789
|
+
}
|
|
2790
|
+
function withSnapshotRetry(p) {
|
|
2791
|
+
if (!p.snapshot)
|
|
2792
|
+
return p;
|
|
2793
|
+
const snapshot = p.snapshot.bind(p);
|
|
2794
|
+
return { ...p, snapshot: () => withSandboxRetry(snapshot) };
|
|
2795
|
+
}
|
|
2796
|
+
function makeE2bSandboxProvider(sb) {
|
|
2797
|
+
return {
|
|
2798
|
+
...makeSandboxProvider(sb),
|
|
2799
|
+
async snapshot() {
|
|
2800
|
+
const { snapshotId } = await sb.createSnapshot();
|
|
2801
|
+
return { snapshotId };
|
|
2802
|
+
}
|
|
2803
|
+
};
|
|
2804
|
+
}
|
|
2650
2805
|
function makeDesktopSandboxProvider(sb) {
|
|
2651
2806
|
return {
|
|
2652
2807
|
...makeSandboxProvider(sb),
|
|
@@ -2704,43 +2859,93 @@ async function parseSseExecStream(body, opts) {
|
|
|
2704
2859
|
throw new Error("Sandbox exec stream ended without an exit event");
|
|
2705
2860
|
return { exitCode, stdout, stderr };
|
|
2706
2861
|
}
|
|
2862
|
+
var DOT_SEGMENT_PATH_RE2 = ".*(?:^|/|%2[fF])(?:\\.|%2[eE]){2}(?:/|%2[fF]|$).*";
|
|
2863
|
+
var DOT_SEGMENT_RE = /(?:^|\/|%2f)(?:\.|%2e){2}(?:\/|%2f|$)/i;
|
|
2864
|
+
function assertSafePathPrefix(domain, prefix) {
|
|
2865
|
+
if (!prefix.startsWith("/") || DOT_SEGMENT_RE.test(prefix)) {
|
|
2866
|
+
throw new Error(`Invalid Tier-2 pathPrefix ${JSON.stringify(prefix)} for "${domain}": prefixes must start with "/" and contain no dot-segments`);
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
function toVercelNetworkPolicy(policy) {
|
|
2870
|
+
if (typeof policy === "string" || !policy.allow || Array.isArray(policy.allow)) {
|
|
2871
|
+
return policy;
|
|
2872
|
+
}
|
|
2873
|
+
const allow = {};
|
|
2874
|
+
for (const [domain, rules] of Object.entries(policy.allow)) {
|
|
2875
|
+
const translated = rules.flatMap((rule) => {
|
|
2876
|
+
const { requestRules, ...rest } = rule;
|
|
2877
|
+
const methods = requestRules?.methods ?? [];
|
|
2878
|
+
const prefixes = requestRules?.pathPrefixes ?? [];
|
|
2879
|
+
if (methods.length === 0 && prefixes.length === 0)
|
|
2880
|
+
return [rest];
|
|
2881
|
+
const methodMatch = methods.length > 0 ? { method: methods } : {};
|
|
2882
|
+
if (prefixes.length === 0)
|
|
2883
|
+
return [{ ...rest, match: methodMatch }];
|
|
2884
|
+
for (const prefix of prefixes)
|
|
2885
|
+
assertSafePathPrefix(domain, prefix);
|
|
2886
|
+
return prefixes.map((prefix) => ({ ...rest, match: { ...methodMatch, path: { startsWith: prefix } } }));
|
|
2887
|
+
});
|
|
2888
|
+
const hasPathGate = rules.some((rule) => (rule.requestRules?.pathPrefixes?.length ?? 0) > 0);
|
|
2889
|
+
allow[domain] = hasPathGate ? [{ match: { path: { regex: DOT_SEGMENT_PATH_RE2 } } }, ...translated] : translated;
|
|
2890
|
+
}
|
|
2891
|
+
return { ...policy, allow };
|
|
2892
|
+
}
|
|
2893
|
+
var VERCEL_MAX_TAGS = 5;
|
|
2894
|
+
function buildVercelTags(metadata) {
|
|
2895
|
+
const tags = { ...metadata, executor: AGENT_COMPOSE_TAG };
|
|
2896
|
+
const extras = Object.keys(tags).filter((k) => k !== "executor").sort();
|
|
2897
|
+
for (const key of extras.slice(VERCEL_MAX_TAGS - 1))
|
|
2898
|
+
delete tags[key];
|
|
2899
|
+
return tags;
|
|
2900
|
+
}
|
|
2707
2901
|
function makeVercelSandboxProvider(sb, globalEnvs) {
|
|
2708
2902
|
const mergeEnvs = (cmdEnvs) => globalEnvs ? { ...globalEnvs, ...cmdEnvs } : cmdEnvs;
|
|
2903
|
+
const asUnavailable = (err, retryable) => {
|
|
2904
|
+
throw new SandboxUnavailableError(err instanceof Error ? err.message : String(err), { retryable, sandboxId: sb.name });
|
|
2905
|
+
};
|
|
2709
2906
|
return {
|
|
2710
|
-
sandboxId: sb.
|
|
2907
|
+
sandboxId: sb.name,
|
|
2711
2908
|
commands: {
|
|
2712
2909
|
async run(cmd, opts) {
|
|
2713
2910
|
const signal = opts?.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined;
|
|
2714
2911
|
let stdout = "";
|
|
2715
2912
|
let stderr = "";
|
|
2716
|
-
const handle = await sb.runCommand({ cmd: "sh", args: ["-c", cmd], cwd: opts?.cwd, env: mergeEnvs(opts?.envs), detached: true, signal, ...opts?.sudo ? { sudo: true } : {} });
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2913
|
+
const handle = await sb.runCommand({ cmd: "sh", args: ["-c", cmd], cwd: opts?.cwd, env: mergeEnvs(opts?.envs), detached: true, signal, ...opts?.sudo ? { sudo: true } : {} }).catch((err) => asUnavailable(err, true));
|
|
2914
|
+
try {
|
|
2915
|
+
await pRetry(async (attempt) => {
|
|
2916
|
+
const h = attempt === 1 ? handle : await sb.getCommand(handle.cmdId);
|
|
2917
|
+
if (attempt > 1) {
|
|
2918
|
+
stdout = "";
|
|
2919
|
+
stderr = "";
|
|
2920
|
+
}
|
|
2921
|
+
for await (const log of h.logs()) {
|
|
2922
|
+
if (log.stream === "stdout") {
|
|
2923
|
+
stdout += log.data;
|
|
2924
|
+
opts?.onStdout?.(log.data);
|
|
2925
|
+
} else {
|
|
2926
|
+
stderr += log.data;
|
|
2927
|
+
opts?.onStderr?.(log.data);
|
|
2928
|
+
}
|
|
2730
2929
|
}
|
|
2930
|
+
}, { retries: 3, minTimeout: 1000, factor: 2 });
|
|
2931
|
+
const finished = await handle.wait();
|
|
2932
|
+
return { exitCode: finished.exitCode, stdout, stderr };
|
|
2933
|
+
} catch (err) {
|
|
2934
|
+
if (signal?.aborted) {
|
|
2935
|
+
throw new Error(`command timed out after ${opts?.timeoutMs}ms: ${cmd.slice(0, 200)}`);
|
|
2731
2936
|
}
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
return { exitCode: finished.exitCode, stdout, stderr };
|
|
2937
|
+
return asUnavailable(err, false);
|
|
2938
|
+
}
|
|
2735
2939
|
}
|
|
2736
2940
|
},
|
|
2737
2941
|
files: {
|
|
2738
2942
|
async write(path, content) {
|
|
2739
|
-
await sb.writeFiles([{ path, content }]);
|
|
2943
|
+
await sb.writeFiles([{ path, content }]).catch((err) => asUnavailable(err, true));
|
|
2740
2944
|
}
|
|
2741
2945
|
},
|
|
2742
2946
|
async kill() {
|
|
2743
2947
|
await sb.stop();
|
|
2948
|
+
await sb.delete();
|
|
2744
2949
|
},
|
|
2745
2950
|
async snapshot() {
|
|
2746
2951
|
const res = await sb.snapshot({ expiration: 0 });
|
|
@@ -2749,6 +2954,9 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
|
|
|
2749
2954
|
snapshotId: res.snapshotId,
|
|
2750
2955
|
...sizeBytes !== undefined ? { sizeBytes } : {}
|
|
2751
2956
|
};
|
|
2957
|
+
},
|
|
2958
|
+
async updateNetworkPolicy(policy) {
|
|
2959
|
+
await sb.update({ networkPolicy: toVercelNetworkPolicy(policy) });
|
|
2752
2960
|
}
|
|
2753
2961
|
};
|
|
2754
2962
|
}
|
|
@@ -2800,8 +3008,10 @@ var SANDBOX_PROVIDERS = {
|
|
|
2800
3008
|
create: async (opts, env) => {
|
|
2801
3009
|
const { Sandbox: VercelSandbox } = await import("@vercel/sandbox");
|
|
2802
3010
|
const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
|
|
2803
|
-
const np = opts.networkPolicy;
|
|
2804
|
-
const
|
|
3011
|
+
const np = opts.networkPolicy ? { networkPolicy: toVercelNetworkPolicy(opts.networkPolicy) } : {};
|
|
3012
|
+
const tags = buildVercelTags(opts.metadata);
|
|
3013
|
+
const tmpl = opts.template ?? process.env.VERCEL_DEFAULT_SNAPSHOT;
|
|
3014
|
+
const sb = await VercelSandbox.create(tmpl ? { source: { type: "snapshot", snapshotId: tmpl }, timeout: opts.timeoutMs, env: opts.envs, tags, persistent: false, ...np, ...creds } : { runtime: "node24", timeout: opts.timeoutMs, env: opts.envs, tags, persistent: false, ...np, ...creds });
|
|
2805
3015
|
return makeVercelSandboxProvider(sb, opts.envs);
|
|
2806
3016
|
},
|
|
2807
3017
|
reconnect: async (sandboxId) => {
|
|
@@ -2811,7 +3021,10 @@ var SANDBOX_PROVIDERS = {
|
|
|
2811
3021
|
teamId: process.env.VERCEL_TEAM_ID ?? "",
|
|
2812
3022
|
projectId: process.env.VERCEL_PROJECT_ID ?? ""
|
|
2813
3023
|
};
|
|
2814
|
-
|
|
3024
|
+
const sb = await VercelSandbox.get({ name: sandboxId, ...creds }).catch((err) => {
|
|
3025
|
+
throw new SandboxUnavailableError(err instanceof Error ? err.message : String(err), { retryable: true, sandboxId });
|
|
3026
|
+
});
|
|
3027
|
+
return makeVercelSandboxProvider(sb);
|
|
2815
3028
|
},
|
|
2816
3029
|
getActiveCount: async (env) => (await SANDBOX_PROVIDERS.vercel.listOwned(env)).length,
|
|
2817
3030
|
listOwned: async (env) => {
|
|
@@ -2819,18 +3032,21 @@ var SANDBOX_PROVIDERS = {
|
|
|
2819
3032
|
const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
|
|
2820
3033
|
const since = Date.now() - VERCEL_VM_LIFETIME_WINDOW_MS;
|
|
2821
3034
|
const out = [];
|
|
2822
|
-
let
|
|
2823
|
-
for (let page = 0;page <
|
|
2824
|
-
const
|
|
2825
|
-
for (const sb of
|
|
3035
|
+
let cursor;
|
|
3036
|
+
for (let page = 0;page < 200; page++) {
|
|
3037
|
+
const result = await pRetry(() => VercelSandbox.list({ ...creds, limit: 50, sortBy: "createdAt", sortOrder: "desc", tags: { executor: AGENT_COMPOSE_TAG }, ...cursor !== undefined ? { cursor } : {} }), { retries: 3, minTimeout: 500, factor: 2 });
|
|
3038
|
+
for (const sb of result.sandboxes) {
|
|
3039
|
+
if (sb.createdAt < since)
|
|
3040
|
+
return out;
|
|
2826
3041
|
if (sb.status === "running") {
|
|
2827
|
-
out.push({ sandboxId: sb.
|
|
3042
|
+
out.push({ sandboxId: sb.name, createdAt: new Date(sb.createdAt), metadata: sb.tags ?? {} });
|
|
2828
3043
|
}
|
|
2829
|
-
|
|
3044
|
+
}
|
|
3045
|
+
if (result.pagination.next === null)
|
|
2830
3046
|
return out;
|
|
2831
|
-
|
|
3047
|
+
cursor = result.pagination.next;
|
|
2832
3048
|
}
|
|
2833
|
-
throw new Error("Vercel listSandboxes exceeded
|
|
3049
|
+
throw new Error("Vercel listSandboxes exceeded 200 pages (10k sandboxes) within the lifetime window — refuse to silently truncate");
|
|
2834
3050
|
},
|
|
2835
3051
|
deleteSnapshot: async (snapshotId, env) => {
|
|
2836
3052
|
const { Snapshot } = await import("@vercel/sandbox");
|
|
@@ -2841,12 +3057,16 @@ var SANDBOX_PROVIDERS = {
|
|
|
2841
3057
|
},
|
|
2842
3058
|
e2b: {
|
|
2843
3059
|
requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
|
|
2844
|
-
create: async ({ template, ...
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
3060
|
+
create: async ({ template, timeoutMs, networkPolicy: _np, ...rest }) => {
|
|
3061
|
+
const maxMs = e2bMaxSandboxMs();
|
|
3062
|
+
if (timeoutMs > maxMs) {
|
|
3063
|
+
console.warn(`[sandbox] e2b create timeout clamped: requested ${timeoutMs}ms exceeds the plan cap ${maxMs}ms — ` + `the sandbox dies at the cap, not the requested deadline. Raise E2B_MAX_SANDBOX_MS on plans that allow more.`);
|
|
3064
|
+
}
|
|
3065
|
+
const sandboxOpts = { ...rest, timeoutMs: Math.min(timeoutMs, maxMs) };
|
|
3066
|
+
const tmpl = template ?? process.env.E2B_DEFAULT_TEMPLATE;
|
|
3067
|
+
return makeE2bSandboxProvider(await (tmpl ? Sandbox.create(tmpl, sandboxOpts) : Sandbox.create(sandboxOpts)));
|
|
2848
3068
|
},
|
|
2849
|
-
reconnect: async (sandboxId) =>
|
|
3069
|
+
reconnect: async (sandboxId) => makeE2bSandboxProvider(await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY ?? "", timeoutMs: 3600000 })),
|
|
2850
3070
|
killAll: async () => {
|
|
2851
3071
|
const paginator = Sandbox.list({ query: { metadata: { executor: AGENT_COMPOSE_TAG } } });
|
|
2852
3072
|
const sandboxes = [];
|
|
@@ -2872,14 +3092,17 @@ var SANDBOX_PROVIDERS = {
|
|
|
2872
3092
|
}
|
|
2873
3093
|
}
|
|
2874
3094
|
return out;
|
|
3095
|
+
},
|
|
3096
|
+
deleteSnapshot: async (snapshotId, env) => {
|
|
3097
|
+
await Sandbox.deleteSnapshot(snapshotId, { apiKey: env.E2B_API_KEY });
|
|
2875
3098
|
}
|
|
2876
3099
|
},
|
|
2877
3100
|
"e2b-desktop": {
|
|
2878
3101
|
requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
|
|
2879
|
-
create: async ({ template, ...
|
|
3102
|
+
create: async ({ template, timeoutMs, networkPolicy: _np, ...rest }) => {
|
|
2880
3103
|
if (!template)
|
|
2881
3104
|
throw new Error("E2B Desktop provider requires an explicit `template` (Dockerfile-based — no default base image)");
|
|
2882
|
-
return makeDesktopSandboxProvider(await Desktop.create(template,
|
|
3105
|
+
return makeDesktopSandboxProvider(await Desktop.create(template, { ...rest, timeoutMs: Math.min(timeoutMs, e2bMaxSandboxMs()) }));
|
|
2883
3106
|
}
|
|
2884
3107
|
}
|
|
2885
3108
|
};
|
|
@@ -2892,13 +3115,14 @@ async function createSandbox(provider, opts) {
|
|
|
2892
3115
|
throw new Error(`Sandbox provider "${provider}" requires env vars:
|
|
2893
3116
|
${missing.join(`
|
|
2894
3117
|
`)}`);
|
|
2895
|
-
|
|
3118
|
+
const env = Object.fromEntries(Object.keys(def.requiredEnv).map((k) => [k, process.env[k]]));
|
|
3119
|
+
return withSnapshotRetry(await def.create(opts, env));
|
|
2896
3120
|
}
|
|
2897
3121
|
async function reconnectSandbox(provider, sandboxId) {
|
|
2898
3122
|
const def = SANDBOX_PROVIDERS[provider];
|
|
2899
3123
|
if (!def?.reconnect)
|
|
2900
3124
|
throw new Error(`Provider "${provider}" does not support reconnect`);
|
|
2901
|
-
return def.reconnect(sandboxId);
|
|
3125
|
+
return withSnapshotRetry(await withSandboxRetry(() => def.reconnect(sandboxId), (error) => error instanceof SandboxNotFoundError ? error.attemptNumber <= 1 : isTransientSandboxError(error)));
|
|
2902
3126
|
}
|
|
2903
3127
|
async function deleteSandboxSnapshot(provider, snapshotId) {
|
|
2904
3128
|
const def = SANDBOX_PROVIDERS[provider];
|
|
@@ -3416,6 +3640,9 @@ function stepInputPath(stepIndex) {
|
|
|
3416
3640
|
function requestContextPath(stepIndex) {
|
|
3417
3641
|
return `/tmp/wf/request-context-${stepIndex}.json`;
|
|
3418
3642
|
}
|
|
3643
|
+
function stepResultFilePath(token) {
|
|
3644
|
+
return `/tmp/wf/step-result-${token}.json`;
|
|
3645
|
+
}
|
|
3419
3646
|
// src/step-invocation/invoker.ts
|
|
3420
3647
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
3421
3648
|
import { z as z10 } from "zod";
|
|
@@ -3471,6 +3698,18 @@ function parseStepResult(stdout, resultToken) {
|
|
|
3471
3698
|
if (pauseLine && resultLine)
|
|
3472
3699
|
break;
|
|
3473
3700
|
}
|
|
3701
|
+
const midLine = (prefix) => {
|
|
3702
|
+
const at = stdout.indexOf(prefix);
|
|
3703
|
+
if (at === -1)
|
|
3704
|
+
return;
|
|
3705
|
+
const end = stdout.indexOf(`
|
|
3706
|
+
`, at);
|
|
3707
|
+
return stdout.slice(at, end === -1 ? undefined : end);
|
|
3708
|
+
};
|
|
3709
|
+
if (!pauseLine)
|
|
3710
|
+
pauseLine = midLine(pausePrefix);
|
|
3711
|
+
if (!resultLine)
|
|
3712
|
+
resultLine = midLine(resultPrefix);
|
|
3474
3713
|
if (pauseLine) {
|
|
3475
3714
|
try {
|
|
3476
3715
|
const raw = JSON.parse(pauseLine.slice(pausePrefix.length));
|
|
@@ -3558,17 +3797,35 @@ async function invokeStep(sandbox, request, opts) {
|
|
|
3558
3797
|
};
|
|
3559
3798
|
const stdoutSplitter = makeLineSplitter(opts?.onStdout, true);
|
|
3560
3799
|
const stderrSplitter = makeLineSplitter(opts?.onStderr, false);
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3800
|
+
let result;
|
|
3801
|
+
try {
|
|
3802
|
+
result = await sandbox.commands.run(RUNNER_COMMAND, {
|
|
3803
|
+
envs,
|
|
3804
|
+
timeoutMs: 0,
|
|
3805
|
+
...stdoutSplitter.onChunk ? { onStdout: stdoutSplitter.onChunk } : {},
|
|
3806
|
+
...stderrSplitter.onChunk ? { onStderr: stderrSplitter.onChunk } : {}
|
|
3807
|
+
});
|
|
3808
|
+
} catch (e) {
|
|
3809
|
+
if (e instanceof SandboxUnavailableError)
|
|
3810
|
+
throw e;
|
|
3811
|
+
const ce = e;
|
|
3812
|
+
result = {
|
|
3813
|
+
stdout: ce.stdout ?? ce.result?.stdout ?? "",
|
|
3814
|
+
stderr: ce.stderr ?? ce.result?.stderr ?? (e instanceof Error ? e.message : String(e)),
|
|
3815
|
+
exitCode: ce.exitCode ?? ce.result?.exitCode ?? 1
|
|
3816
|
+
};
|
|
3817
|
+
}
|
|
3567
3818
|
stdoutSplitter.flush();
|
|
3568
3819
|
stderrSplitter.flush();
|
|
3569
3820
|
const parsed = parseStepResult(result.stdout, resultToken);
|
|
3570
3821
|
if (parsed)
|
|
3571
3822
|
return parsed;
|
|
3823
|
+
const fileRead = await sandbox.commands.run(`cat ${stepResultFilePath(resultToken)} 2>/dev/null || true`).catch(() => null);
|
|
3824
|
+
if (fileRead?.stdout) {
|
|
3825
|
+
const fromFile = parseStepResult(fileRead.stdout, resultToken);
|
|
3826
|
+
if (fromFile)
|
|
3827
|
+
return fromFile;
|
|
3828
|
+
}
|
|
3572
3829
|
if (result.exitCode !== 0) {
|
|
3573
3830
|
const stderrTail = result.stderr.trim().slice(-2000);
|
|
3574
3831
|
const stdoutTail = result.stdout.trim().slice(-2000);
|
|
@@ -3589,13 +3846,19 @@ ${details}` : ""}`,
|
|
|
3589
3846
|
}
|
|
3590
3847
|
};
|
|
3591
3848
|
}
|
|
3849
|
+
const tail = result.stdout.trim().slice(-500);
|
|
3592
3850
|
return {
|
|
3593
3851
|
ok: false,
|
|
3594
|
-
error: {
|
|
3852
|
+
error: {
|
|
3853
|
+
kind: "protocol",
|
|
3854
|
+
message: `no tokenised step result on stdout or in the result file (runner exited 0 without emitting)${tail ? `
|
|
3855
|
+
stdout tail:
|
|
3856
|
+
${tail}` : ""}`
|
|
3857
|
+
}
|
|
3595
3858
|
};
|
|
3596
3859
|
}
|
|
3597
3860
|
// src/step-invocation/server.ts
|
|
3598
|
-
import { readFileSync } from "node:fs";
|
|
3861
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3599
3862
|
function writeSentinelLine(line) {
|
|
3600
3863
|
return new Promise((resolve, reject) => {
|
|
3601
3864
|
process.stdout.write(line, (err) => {
|
|
@@ -3606,9 +3869,21 @@ function writeSentinelLine(line) {
|
|
|
3606
3869
|
});
|
|
3607
3870
|
});
|
|
3608
3871
|
}
|
|
3609
|
-
function
|
|
3610
|
-
|
|
3872
|
+
function persistSentinelFile(token, line) {
|
|
3873
|
+
try {
|
|
3874
|
+
mkdirSync("/tmp/wf", { recursive: true });
|
|
3875
|
+
writeFileSync(stepResultFilePath(token), line);
|
|
3876
|
+
} catch (err) {
|
|
3877
|
+
process.stderr.write(`[step-server] result-file persist failed: ${err instanceof Error ? err.message : String(err)}
|
|
3611
3878
|
`);
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
function emitResult(token, payload) {
|
|
3882
|
+
const line = `${STEP_RESULT_PREFIX}${token}:${JSON.stringify(payload)}
|
|
3883
|
+
`;
|
|
3884
|
+
persistSentinelFile(token, line);
|
|
3885
|
+
return writeSentinelLine(`
|
|
3886
|
+
${line}`);
|
|
3612
3887
|
}
|
|
3613
3888
|
function emitPause(token, signal) {
|
|
3614
3889
|
const body = JSON.stringify({
|
|
@@ -3616,8 +3891,11 @@ function emitPause(token, signal) {
|
|
|
3616
3891
|
pauseRequest: signal.pauseRequest,
|
|
3617
3892
|
...signal.observability ? { observability: signal.observability } : {}
|
|
3618
3893
|
});
|
|
3619
|
-
|
|
3620
|
-
|
|
3894
|
+
const line = `${STEP_PAUSE_PREFIX}${token}:${body}
|
|
3895
|
+
`;
|
|
3896
|
+
persistSentinelFile(token, line);
|
|
3897
|
+
return writeSentinelLine(`
|
|
3898
|
+
${line}`);
|
|
3621
3899
|
}
|
|
3622
3900
|
function scrubProtocolEnvs() {
|
|
3623
3901
|
for (const key of [
|