@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/dist/index.js CHANGED
@@ -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.memory !== undefined)
546
- out.memory = source.memory;
547
- if (source.postRunHooks !== undefined)
548
- out.postRunHooks = Object.freeze([...source.postRunHooks]);
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 = 1;
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.memory !== undefined ? { memory: metadata.memory } : {},
1188
- ...metadata.postRunHooks !== undefined ? { postRunHooks: metadata.postRunHooks } : {}
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-opus-4-7";
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 ?? 40;
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: iteration > 0 ? lastSessionId : undefined,
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.is_error || message.subtype === "error_during_execution") {
2253
- msgs.push({ type: "error", text: formatError(message.error ?? message.result ?? message.message), timestamp: ts });
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 ?? DEFAULT_CLAUDE_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.config.model ?? this.options.model ?? DEFAULT_CLAUDE_MODEL,
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
- yield { type: "error", text: formatError(err), timestamp: now2() };
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.sandboxId,
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
- await pRetry(async (attempt) => {
2738
- const h = attempt === 1 ? handle : await sb.getCommand(handle.cmdId);
2739
- if (attempt > 1) {
2740
- stdout = "";
2741
- stderr = "";
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
- }, { retries: 3, minTimeout: 1000, factor: 2 });
2753
- const finished = await handle.wait();
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 sb = await VercelSandbox.create(opts.template ? { source: { type: "snapshot", snapshotId: opts.template }, timeout: opts.timeoutMs, env: opts.envs, ...np ? { networkPolicy: np } : {}, ...creds } : { runtime: "node24", timeout: opts.timeoutMs, env: opts.envs, ...np ? { networkPolicy: np } : {}, ...creds });
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
- return makeVercelSandboxProvider(await VercelSandbox.get({ sandboxId, ...creds }));
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 until;
2843
- for (let page = 0;page < 50; page++) {
2844
- const { json } = await pRetry(() => VercelSandbox.list({ ...creds, limit: 200, since, ...until !== undefined ? { until } : {} }), { retries: 3, minTimeout: 500, factor: 2 });
2845
- for (const sb of json.sandboxes)
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.id, createdAt: new Date(sb.createdAt), metadata: {} });
3042
+ out.push({ sandboxId: sb.name, createdAt: new Date(sb.createdAt), metadata: sb.tags ?? {} });
2848
3043
  }
2849
- if (json.pagination.next === null)
3044
+ }
3045
+ if (result.pagination.next === null)
2850
3046
  return out;
2851
- until = json.pagination.next;
3047
+ cursor = result.pagination.next;
2852
3048
  }
2853
- throw new Error("Vercel listSandboxes exceeded 50 pages within the lifetime window — refuse to silently truncate");
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, ...sandboxOpts }) => {
2865
- if (!template)
2866
- throw new Error("E2B provider requires an explicit `template` (Dockerfile-based — no default base image)");
2867
- return makeSandboxProvider(await Sandbox.create(template, sandboxOpts));
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) => makeSandboxProvider(await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY ?? "", timeoutMs: 3600000 })),
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, ...sandboxOpts }) => {
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, sandboxOpts));
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
- return def.create(opts, Object.fromEntries(Object.keys(def.requiredEnv).map((k) => [k, process.env[k]])));
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
- const result = await sandbox.commands.run(RUNNER_COMMAND, {
3582
- envs,
3583
- timeoutMs: 0,
3584
- ...stdoutSplitter.onChunk ? { onStdout: stdoutSplitter.onChunk } : {},
3585
- ...stderrSplitter.onChunk ? { onStderr: stderrSplitter.onChunk } : {}
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: { kind: "protocol", message: "no tokenised step result found on stdout" }
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 emitResult(token, payload) {
3630
- return writeSentinelLine(`${STEP_RESULT_PREFIX}${token}:${JSON.stringify(payload)}
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
- return writeSentinelLine(`${STEP_PAUSE_PREFIX}${token}:${body}
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 [
@@ -3969,9 +4227,11 @@ export {
3969
4227
  TMP_DIR,
3970
4228
  StepValidationError,
3971
4229
  StepExecutionError,
4230
+ SandboxUnavailableError,
3972
4231
  STEP_RESULT_PREFIX,
3973
4232
  STEP_PAUSE_PREFIX,
3974
4233
  STEP_ENV,
4234
+ SANDBOX_UNAVAILABLE_PREFIX,
3975
4235
  ReservedKeyError,
3976
4236
  RequestContext,
3977
4237
  RUNNER_COMMAND,