@agent-compose/sdk 0.5.6 → 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 +335 -75
- package/dist/runtimes/claude.d.ts +9 -1
- package/dist/runtimes/openai-desktop.js +333 -75
- 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 +69 -12
- 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 +3 -2
- package/src/runtimes/claude.ts +66 -8
- 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 +77 -15
- 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
|
}
|
|
@@ -2633,9 +2725,25 @@ var amp_default = createAmpRuntime();
|
|
|
2633
2725
|
import { promises as fs2 } from "node:fs";
|
|
2634
2726
|
import { dirname as dirname2 } from "node:path";
|
|
2635
2727
|
import { spawn } from "node:child_process";
|
|
2636
|
-
import { Sandbox } from "e2b";
|
|
2728
|
+
import { Sandbox, SandboxNotFoundError, RateLimitError } from "e2b";
|
|
2637
2729
|
import { Sandbox as Desktop } from "@e2b/desktop";
|
|
2638
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
|
|
2639
2747
|
var AGENT_COMPOSE_TAG = process.env.AGENT_COMPOSE_TAG ?? `agent-compose-${process.env.AGENT_COMPOSE_ENV ?? "dev"}`;
|
|
2640
2748
|
var VERCEL_VM_LIFETIME_WINDOW_MS = 6 * 60 * 60 * 1000;
|
|
2641
2749
|
function makeSandboxProvider(sb) {
|
|
@@ -2667,6 +2775,33 @@ function makeSandboxProvider(sb) {
|
|
|
2667
2775
|
kill: () => sb.kill()
|
|
2668
2776
|
};
|
|
2669
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
|
+
}
|
|
2670
2805
|
function makeDesktopSandboxProvider(sb) {
|
|
2671
2806
|
return {
|
|
2672
2807
|
...makeSandboxProvider(sb),
|
|
@@ -2724,43 +2859,93 @@ async function parseSseExecStream(body, opts) {
|
|
|
2724
2859
|
throw new Error("Sandbox exec stream ended without an exit event");
|
|
2725
2860
|
return { exitCode, stdout, stderr };
|
|
2726
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
|
+
}
|
|
2727
2901
|
function makeVercelSandboxProvider(sb, globalEnvs) {
|
|
2728
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
|
+
};
|
|
2729
2906
|
return {
|
|
2730
|
-
sandboxId: sb.
|
|
2907
|
+
sandboxId: sb.name,
|
|
2731
2908
|
commands: {
|
|
2732
2909
|
async run(cmd, opts) {
|
|
2733
2910
|
const signal = opts?.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined;
|
|
2734
2911
|
let stdout = "";
|
|
2735
2912
|
let stderr = "";
|
|
2736
|
-
const handle = await sb.runCommand({ cmd: "sh", args: ["-c", cmd], cwd: opts?.cwd, env: mergeEnvs(opts?.envs), detached: true, signal, ...opts?.sudo ? { sudo: true } : {} });
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
for await (const log of h.logs()) {
|
|
2744
|
-
if (log.stream === "stdout") {
|
|
2745
|
-
stdout += log.data;
|
|
2746
|
-
opts?.onStdout?.(log.data);
|
|
2747
|
-
} else {
|
|
2748
|
-
stderr += log.data;
|
|
2749
|
-
opts?.onStderr?.(log.data);
|
|
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 = "";
|
|
2750
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
|
+
}
|
|
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)}`);
|
|
2751
2936
|
}
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
return { exitCode: finished.exitCode, stdout, stderr };
|
|
2937
|
+
return asUnavailable(err, false);
|
|
2938
|
+
}
|
|
2755
2939
|
}
|
|
2756
2940
|
},
|
|
2757
2941
|
files: {
|
|
2758
2942
|
async write(path, content) {
|
|
2759
|
-
await sb.writeFiles([{ path, content }]);
|
|
2943
|
+
await sb.writeFiles([{ path, content }]).catch((err) => asUnavailable(err, true));
|
|
2760
2944
|
}
|
|
2761
2945
|
},
|
|
2762
2946
|
async kill() {
|
|
2763
2947
|
await sb.stop();
|
|
2948
|
+
await sb.delete();
|
|
2764
2949
|
},
|
|
2765
2950
|
async snapshot() {
|
|
2766
2951
|
const res = await sb.snapshot({ expiration: 0 });
|
|
@@ -2769,6 +2954,9 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
|
|
|
2769
2954
|
snapshotId: res.snapshotId,
|
|
2770
2955
|
...sizeBytes !== undefined ? { sizeBytes } : {}
|
|
2771
2956
|
};
|
|
2957
|
+
},
|
|
2958
|
+
async updateNetworkPolicy(policy) {
|
|
2959
|
+
await sb.update({ networkPolicy: toVercelNetworkPolicy(policy) });
|
|
2772
2960
|
}
|
|
2773
2961
|
};
|
|
2774
2962
|
}
|
|
@@ -2820,8 +3008,10 @@ var SANDBOX_PROVIDERS = {
|
|
|
2820
3008
|
create: async (opts, env) => {
|
|
2821
3009
|
const { Sandbox: VercelSandbox } = await import("@vercel/sandbox");
|
|
2822
3010
|
const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
|
|
2823
|
-
const np = opts.networkPolicy;
|
|
2824
|
-
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 });
|
|
2825
3015
|
return makeVercelSandboxProvider(sb, opts.envs);
|
|
2826
3016
|
},
|
|
2827
3017
|
reconnect: async (sandboxId) => {
|
|
@@ -2831,7 +3021,10 @@ var SANDBOX_PROVIDERS = {
|
|
|
2831
3021
|
teamId: process.env.VERCEL_TEAM_ID ?? "",
|
|
2832
3022
|
projectId: process.env.VERCEL_PROJECT_ID ?? ""
|
|
2833
3023
|
};
|
|
2834
|
-
|
|
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);
|
|
2835
3028
|
},
|
|
2836
3029
|
getActiveCount: async (env) => (await SANDBOX_PROVIDERS.vercel.listOwned(env)).length,
|
|
2837
3030
|
listOwned: async (env) => {
|
|
@@ -2839,18 +3032,21 @@ var SANDBOX_PROVIDERS = {
|
|
|
2839
3032
|
const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
|
|
2840
3033
|
const since = Date.now() - VERCEL_VM_LIFETIME_WINDOW_MS;
|
|
2841
3034
|
const out = [];
|
|
2842
|
-
let
|
|
2843
|
-
for (let page = 0;page <
|
|
2844
|
-
const
|
|
2845
|
-
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;
|
|
2846
3041
|
if (sb.status === "running") {
|
|
2847
|
-
out.push({ sandboxId: sb.
|
|
3042
|
+
out.push({ sandboxId: sb.name, createdAt: new Date(sb.createdAt), metadata: sb.tags ?? {} });
|
|
2848
3043
|
}
|
|
2849
|
-
|
|
3044
|
+
}
|
|
3045
|
+
if (result.pagination.next === null)
|
|
2850
3046
|
return out;
|
|
2851
|
-
|
|
3047
|
+
cursor = result.pagination.next;
|
|
2852
3048
|
}
|
|
2853
|
-
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");
|
|
2854
3050
|
},
|
|
2855
3051
|
deleteSnapshot: async (snapshotId, env) => {
|
|
2856
3052
|
const { Snapshot } = await import("@vercel/sandbox");
|
|
@@ -2861,12 +3057,16 @@ var SANDBOX_PROVIDERS = {
|
|
|
2861
3057
|
},
|
|
2862
3058
|
e2b: {
|
|
2863
3059
|
requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
|
|
2864
|
-
create: async ({ template, ...
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
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)));
|
|
2868
3068
|
},
|
|
2869
|
-
reconnect: async (sandboxId) =>
|
|
3069
|
+
reconnect: async (sandboxId) => makeE2bSandboxProvider(await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY ?? "", timeoutMs: 3600000 })),
|
|
2870
3070
|
killAll: async () => {
|
|
2871
3071
|
const paginator = Sandbox.list({ query: { metadata: { executor: AGENT_COMPOSE_TAG } } });
|
|
2872
3072
|
const sandboxes = [];
|
|
@@ -2892,14 +3092,17 @@ var SANDBOX_PROVIDERS = {
|
|
|
2892
3092
|
}
|
|
2893
3093
|
}
|
|
2894
3094
|
return out;
|
|
3095
|
+
},
|
|
3096
|
+
deleteSnapshot: async (snapshotId, env) => {
|
|
3097
|
+
await Sandbox.deleteSnapshot(snapshotId, { apiKey: env.E2B_API_KEY });
|
|
2895
3098
|
}
|
|
2896
3099
|
},
|
|
2897
3100
|
"e2b-desktop": {
|
|
2898
3101
|
requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
|
|
2899
|
-
create: async ({ template, ...
|
|
3102
|
+
create: async ({ template, timeoutMs, networkPolicy: _np, ...rest }) => {
|
|
2900
3103
|
if (!template)
|
|
2901
3104
|
throw new Error("E2B Desktop provider requires an explicit `template` (Dockerfile-based — no default base image)");
|
|
2902
|
-
return makeDesktopSandboxProvider(await Desktop.create(template,
|
|
3105
|
+
return makeDesktopSandboxProvider(await Desktop.create(template, { ...rest, timeoutMs: Math.min(timeoutMs, e2bMaxSandboxMs()) }));
|
|
2903
3106
|
}
|
|
2904
3107
|
}
|
|
2905
3108
|
};
|
|
@@ -2912,13 +3115,14 @@ async function createSandbox(provider, opts) {
|
|
|
2912
3115
|
throw new Error(`Sandbox provider "${provider}" requires env vars:
|
|
2913
3116
|
${missing.join(`
|
|
2914
3117
|
`)}`);
|
|
2915
|
-
|
|
3118
|
+
const env = Object.fromEntries(Object.keys(def.requiredEnv).map((k) => [k, process.env[k]]));
|
|
3119
|
+
return withSnapshotRetry(await def.create(opts, env));
|
|
2916
3120
|
}
|
|
2917
3121
|
async function reconnectSandbox(provider, sandboxId) {
|
|
2918
3122
|
const def = SANDBOX_PROVIDERS[provider];
|
|
2919
3123
|
if (!def?.reconnect)
|
|
2920
3124
|
throw new Error(`Provider "${provider}" does not support reconnect`);
|
|
2921
|
-
return def.reconnect(sandboxId);
|
|
3125
|
+
return withSnapshotRetry(await withSandboxRetry(() => def.reconnect(sandboxId), (error) => error instanceof SandboxNotFoundError ? error.attemptNumber <= 1 : isTransientSandboxError(error)));
|
|
2922
3126
|
}
|
|
2923
3127
|
async function deleteSandboxSnapshot(provider, snapshotId) {
|
|
2924
3128
|
const def = SANDBOX_PROVIDERS[provider];
|
|
@@ -3436,6 +3640,9 @@ function stepInputPath(stepIndex) {
|
|
|
3436
3640
|
function requestContextPath(stepIndex) {
|
|
3437
3641
|
return `/tmp/wf/request-context-${stepIndex}.json`;
|
|
3438
3642
|
}
|
|
3643
|
+
function stepResultFilePath(token) {
|
|
3644
|
+
return `/tmp/wf/step-result-${token}.json`;
|
|
3645
|
+
}
|
|
3439
3646
|
// src/step-invocation/invoker.ts
|
|
3440
3647
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
3441
3648
|
import { z as z10 } from "zod";
|
|
@@ -3491,6 +3698,18 @@ function parseStepResult(stdout, resultToken) {
|
|
|
3491
3698
|
if (pauseLine && resultLine)
|
|
3492
3699
|
break;
|
|
3493
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);
|
|
3494
3713
|
if (pauseLine) {
|
|
3495
3714
|
try {
|
|
3496
3715
|
const raw = JSON.parse(pauseLine.slice(pausePrefix.length));
|
|
@@ -3578,17 +3797,35 @@ async function invokeStep(sandbox, request, opts) {
|
|
|
3578
3797
|
};
|
|
3579
3798
|
const stdoutSplitter = makeLineSplitter(opts?.onStdout, true);
|
|
3580
3799
|
const stderrSplitter = makeLineSplitter(opts?.onStderr, false);
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
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
|
+
}
|
|
3587
3818
|
stdoutSplitter.flush();
|
|
3588
3819
|
stderrSplitter.flush();
|
|
3589
3820
|
const parsed = parseStepResult(result.stdout, resultToken);
|
|
3590
3821
|
if (parsed)
|
|
3591
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
|
+
}
|
|
3592
3829
|
if (result.exitCode !== 0) {
|
|
3593
3830
|
const stderrTail = result.stderr.trim().slice(-2000);
|
|
3594
3831
|
const stdoutTail = result.stdout.trim().slice(-2000);
|
|
@@ -3609,13 +3846,19 @@ ${details}` : ""}`,
|
|
|
3609
3846
|
}
|
|
3610
3847
|
};
|
|
3611
3848
|
}
|
|
3849
|
+
const tail = result.stdout.trim().slice(-500);
|
|
3612
3850
|
return {
|
|
3613
3851
|
ok: false,
|
|
3614
|
-
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
|
+
}
|
|
3615
3858
|
};
|
|
3616
3859
|
}
|
|
3617
3860
|
// src/step-invocation/server.ts
|
|
3618
|
-
import { readFileSync } from "node:fs";
|
|
3861
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3619
3862
|
function writeSentinelLine(line) {
|
|
3620
3863
|
return new Promise((resolve, reject) => {
|
|
3621
3864
|
process.stdout.write(line, (err) => {
|
|
@@ -3626,9 +3869,21 @@ function writeSentinelLine(line) {
|
|
|
3626
3869
|
});
|
|
3627
3870
|
});
|
|
3628
3871
|
}
|
|
3629
|
-
function
|
|
3630
|
-
|
|
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)}
|
|
3631
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}`);
|
|
3632
3887
|
}
|
|
3633
3888
|
function emitPause(token, signal) {
|
|
3634
3889
|
const body = JSON.stringify({
|
|
@@ -3636,8 +3891,11 @@ function emitPause(token, signal) {
|
|
|
3636
3891
|
pauseRequest: signal.pauseRequest,
|
|
3637
3892
|
...signal.observability ? { observability: signal.observability } : {}
|
|
3638
3893
|
});
|
|
3639
|
-
|
|
3640
|
-
|
|
3894
|
+
const line = `${STEP_PAUSE_PREFIX}${token}:${body}
|
|
3895
|
+
`;
|
|
3896
|
+
persistSentinelFile(token, line);
|
|
3897
|
+
return writeSentinelLine(`
|
|
3898
|
+
${line}`);
|
|
3641
3899
|
}
|
|
3642
3900
|
function scrubProtocolEnvs() {
|
|
3643
3901
|
for (const key of [
|