@agent-compose/sdk 0.5.6 → 0.5.8

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.
Files changed (44) hide show
  1. package/README.md +4 -4
  2. package/dist/agent/__tests__/run-agent-liveness.test.d.ts +17 -0
  3. package/dist/agent/agent-context.d.ts +67 -0
  4. package/dist/agent/agent-loop-contract.test.d.ts +1 -0
  5. package/dist/agent/agent-loop.d.ts +2 -1
  6. package/dist/client.d.ts +129 -24
  7. package/dist/index.d.ts +9 -4
  8. package/dist/index.js +553 -89
  9. package/dist/pause/wrappers.d.ts +7 -11
  10. package/dist/runtimes/claude.d.ts +9 -1
  11. package/dist/runtimes/openai-desktop.js +548 -89
  12. package/dist/sandbox-errors.d.ts +49 -0
  13. package/dist/sandbox.d.ts +92 -13
  14. package/dist/step-invocation/protocol.d.ts +6 -0
  15. package/dist/step-invocation/types.d.ts +1 -1
  16. package/dist/types/execution-context.d.ts +1 -3
  17. package/dist/types/sandbox-environment.d.ts +1 -10
  18. package/dist/types/sandbox.d.ts +27 -3
  19. package/dist/types/workflow-metadata.d.ts +81 -13
  20. package/dist/types/workflow.d.ts +45 -10
  21. package/dist/utils/bundler.d.ts +40 -9
  22. package/dist/workflow-steps/workflow.d.ts +4 -3
  23. package/package.json +2 -2
  24. package/src/agent/agent-context.ts +212 -0
  25. package/src/agent/agent-loop.ts +78 -10
  26. package/src/agent/run-agent.ts +37 -1
  27. package/src/client.ts +232 -26
  28. package/src/index.ts +9 -4
  29. package/src/pause/wrappers.ts +7 -21
  30. package/src/runtimes/claude.ts +66 -8
  31. package/src/sandbox-errors.ts +53 -0
  32. package/src/sandbox.ts +438 -61
  33. package/src/step-invocation/invoker.ts +66 -8
  34. package/src/step-invocation/protocol.ts +9 -0
  35. package/src/step-invocation/server.ts +27 -4
  36. package/src/step-invocation/types.ts +1 -1
  37. package/src/types/execution-context.ts +1 -3
  38. package/src/types/sandbox-environment.ts +1 -11
  39. package/src/types/sandbox.ts +28 -3
  40. package/src/types/workflow-metadata.ts +91 -16
  41. package/src/types/workflow.ts +45 -12
  42. package/src/utils/bundler.ts +46 -13
  43. package/src/workflow-steps/workflow.ts +4 -3
  44. package/src/workflows/invoke-child.ts +7 -1
package/dist/index.js CHANGED
@@ -540,12 +540,16 @@ function extractMetadata(source) {
540
540
  out.outputSchema = freezeMetadataValue(source.outputSchema);
541
541
  if (source.snapshots !== undefined)
542
542
  out.snapshots = Object.freeze({ ...source.snapshots });
543
+ if (source.resources !== undefined)
544
+ out.resources = Object.freeze({ ...source.resources });
543
545
  if (source.processors !== undefined)
544
546
  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]);
547
+ if (source.connectors !== undefined)
548
+ out.connectors = freezeMetadataValue(source.connectors);
549
+ if (source.connectorOperation !== undefined)
550
+ out.connectorOperation = Object.freeze({ ...source.connectorOperation });
551
+ if (source.invokePolicy !== undefined)
552
+ out.invokePolicy = freezeMetadataValue(source.invokePolicy);
549
553
  return Object.freeze(out);
550
554
  }
551
555
  function freezeMetadataValue(value) {
@@ -615,7 +619,6 @@ function compileRunForm(def, metadata) {
615
619
  agentEvents: stepCtx.agentEvents,
616
620
  checkpoint: stepCtx.checkpoint,
617
621
  pause: stepCtx.pause,
618
- requestDecision: stepCtx.requestDecision,
619
622
  sleep: stepCtx.sleep,
620
623
  waitForEvent: stepCtx.waitForEvent,
621
624
  processors: metadata.processors ?? []
@@ -649,7 +652,6 @@ function defineSandboxEnvironment(env) {
649
652
  return defineWorkflow({
650
653
  ...env.description !== undefined ? { description: env.description } : {},
651
654
  snapshots: env.snapshots ?? { saveLatest: true },
652
- memory: env.memory ?? false,
653
655
  run: async (_ctx, sandbox) => env.setup(sandbox)
654
656
  });
655
657
  }
@@ -800,15 +802,18 @@ class AgentComposeClient {
800
802
  invoke(name, input, opts) {
801
803
  const parentRunId = opts?.parentRunId === undefined ? detectAmbientParentRunId() : opts.parentRunId;
802
804
  const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
805
+ const headers = {};
806
+ if (opts?.idempotencyKey)
807
+ headers["Idempotency-Key"] = opts.idempotencyKey;
803
808
  return this.fetch(templatePath(factorySlug, name, "invoke"), {
804
809
  method: "POST",
810
+ ...opts?.idempotencyKey ? { headers } : {},
805
811
  body: {
806
812
  input,
807
813
  ...opts?.snapshots !== undefined ? { snapshots: opts.snapshots } : {},
808
814
  ...opts?.networkPolicy !== undefined ? { networkPolicy: opts.networkPolicy } : {},
809
815
  ...opts?.placeholders !== undefined ? { placeholders: opts.placeholders } : {},
810
- ...opts?.memory !== undefined ? { memory: opts.memory } : {},
811
- ...opts?.postRunHooks !== undefined ? { postRunHooks: opts.postRunHooks } : {},
816
+ ...opts?.size !== undefined ? { size: opts.size } : {},
812
817
  ...parentRunId ? { parentRunId } : {},
813
818
  ...opts?.agentId ? { agentId: opts.agentId } : {}
814
819
  }
@@ -933,6 +938,56 @@ class AgentComposeClient {
933
938
  const body = await this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/events`);
934
939
  return body.events;
935
940
  }
941
+ async listRunArtifacts(runId) {
942
+ const body = await this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/artifacts`);
943
+ return body.artifacts.map((a) => ({
944
+ path: a.path,
945
+ factorySlug: a.factory_slug,
946
+ sizeBytes: a.size_bytes,
947
+ lastWriteAt: a.last_write_at,
948
+ preview: a.preview
949
+ }));
950
+ }
951
+ async putFactoryFile(path, content, opts) {
952
+ const factorySlug = opts?.factorySlug ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined) ?? DEFAULT_FACTORY;
953
+ const runToken = typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_RUN_TOKEN : undefined;
954
+ const wire = await this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/content?path=${encodeURIComponent(path)}`, {
955
+ method: "PUT",
956
+ body: content,
957
+ headers: {
958
+ "content-type": opts?.contentType ?? "text/plain; charset=utf-8",
959
+ ...runToken ? { "x-run-token": runToken } : {}
960
+ }
961
+ });
962
+ return {
963
+ path: wire.path,
964
+ contentHash: wire.content_hash,
965
+ sizeBytes: wire.size_bytes,
966
+ created: wire.created
967
+ };
968
+ }
969
+ async getFactoryFile(path, opts) {
970
+ const factorySlug = opts?.factorySlug ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined) ?? DEFAULT_FACTORY;
971
+ const q2 = new URLSearchParams({ path });
972
+ if (opts?.revision !== undefined)
973
+ q2.set("revision", String(opts.revision));
974
+ return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/content?${q2}`, { responseType: "text" });
975
+ }
976
+ async listMembers() {
977
+ const body = await this.fetch("/api/v1/team/members");
978
+ return body.members;
979
+ }
980
+ async createMentions(input) {
981
+ const factorySlug = input.factorySlug ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined) ?? DEFAULT_FACTORY;
982
+ const runToken = typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_RUN_TOKEN : undefined;
983
+ const { factorySlug: _omit, ...payload } = input;
984
+ const body = await this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/mentions`, {
985
+ method: "POST",
986
+ body: payload,
987
+ ...runToken ? { headers: { "x-run-token": runToken } } : {}
988
+ });
989
+ return body.mentions;
990
+ }
936
991
  async listFactoryEvents(opts) {
937
992
  const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
938
993
  const q2 = new URLSearchParams;
@@ -1068,7 +1123,7 @@ function workflowPlan(steps) {
1068
1123
  }
1069
1124
 
1070
1125
  // src/utils/bundler.ts
1071
- var BUNDLER_VERSION = 1;
1126
+ var BUNDLER_VERSION = 2;
1072
1127
 
1073
1128
  class WorkflowSourceValidationError extends Error {
1074
1129
  constructor(message) {
@@ -1184,8 +1239,10 @@ async function bundleWorkflow(workflowPath, overrides) {
1184
1239
  ...inputSchema !== undefined ? { inputSchema } : {},
1185
1240
  ...outputSchema !== undefined ? { outputSchema } : {},
1186
1241
  ...metadata.snapshots !== undefined ? { snapshots: metadata.snapshots } : {},
1187
- ...metadata.memory !== undefined ? { memory: metadata.memory } : {},
1188
- ...metadata.postRunHooks !== undefined ? { postRunHooks: metadata.postRunHooks } : {}
1242
+ ...metadata.resources !== undefined ? { resources: metadata.resources } : {},
1243
+ ...metadata.connectors !== undefined ? { connectors: metadata.connectors } : {},
1244
+ ...metadata.connectorOperation !== undefined ? { connectorOperation: metadata.connectorOperation } : {},
1245
+ ...metadata.invokePolicy !== undefined ? { invokePolicy: metadata.invokePolicy } : {}
1189
1246
  };
1190
1247
  }
1191
1248
  function extractIOSchema(zodSchema) {
@@ -1812,9 +1869,9 @@ async function runControlPoller(opts) {
1812
1869
  }
1813
1870
 
1814
1871
  // src/agent/agent-loop.ts
1815
- var DEFAULT_CLAUDE_MODEL = "claude-opus-4-7";
1872
+ var DEFAULT_CLAUDE_MODEL = "claude-fable-5";
1816
1873
  var SAME_BLOCKER_ITERATIONS = 3;
1817
- var STALL_ITERATIONS = 3;
1874
+ var STALL_ITERATIONS = 6;
1818
1875
  var MESSAGE_PREVIEW_CHARS = 400;
1819
1876
  function parseAgentStatus(text) {
1820
1877
  const match = text.match(/<status>([\s\S]*?)<\/status>/);
@@ -1871,8 +1928,9 @@ async function agentLoop(opts) {
1871
1928
  const label = opts.label ?? "agent";
1872
1929
  const logLabel = opts.label ?? "[Agent Loop]";
1873
1930
  const startedAt = Date.now();
1874
- const turnsPerIteration = opts.turnsPerIteration ?? 40;
1875
- const maxIterations = opts.maxIterations ?? 8;
1931
+ const turnsPerIteration = opts.turnsPerIteration;
1932
+ const maxIterations = opts.maxIterations ?? (turnsPerIteration === undefined ? 1 : 8);
1933
+ let schemaRetriesLeft = 10;
1876
1934
  const processors = opts.processors ?? [];
1877
1935
  const requestContext = opts.requestContext ?? RequestContext.fromReserved({
1878
1936
  teamId: "",
@@ -1893,7 +1951,7 @@ async function agentLoop(opts) {
1893
1951
  if (!opts.runtime)
1894
1952
  throw new Error("agentLoop: opts.runtime is required");
1895
1953
  const client = opts.runtime({
1896
- maxTurns: turnsPerIteration,
1954
+ ...turnsPerIteration !== undefined ? { maxTurns: turnsPerIteration } : {},
1897
1955
  allowedTools: opts.allowedTools ?? DEFAULT_ALLOWED_TOOLS,
1898
1956
  label: logLabel,
1899
1957
  cwd: opts.cwd,
@@ -2002,6 +2060,15 @@ async function agentLoop(opts) {
2002
2060
  }
2003
2061
  const procCtx = buildProcCtx(iteration + 1);
2004
2062
  let initialPrompt = opts.buildPrompt(lastStatus, iteration);
2063
+ if (lastResponseValidationError) {
2064
+ initialPrompt = `${initialPrompt}
2065
+
2066
+ [response contract violation — fix and re-emit]
2067
+ Your previous <response> failed schema validation with these errors:
2068
+ ${lastResponseValidationError.slice(0, 2000)}
2069
+ Re-emit the COMPLETE corrected <response> JSON now: every required field present, correctly named and typed (no omissions, no renames).`;
2070
+ lastResponseValidationError = "";
2071
+ }
2005
2072
  if (steerDecision) {
2006
2073
  const who = steerDecision.actor ? ` from ${steerDecision.actor}` : "";
2007
2074
  initialPrompt = `${initialPrompt}
@@ -2014,12 +2081,12 @@ async function agentLoop(opts) {
2014
2081
  throw new Error(`${logLabel} input ${inputVerdict.kind === "deny" ? "rejected" : "aborted"}: ${inputVerdict.reason}`);
2015
2082
  }
2016
2083
  const prompt = inputVerdict.value;
2017
- process.stdout.write(`${logLabel} iteration ${iteration + 1}/${maxIterations} · ${turnsPerIteration} turns
2084
+ process.stdout.write(`${logLabel} iteration ${iteration + 1}/${maxIterations} · ${turnsPerIteration !== undefined ? `${turnsPerIteration} turns` : "harness-decided turns"}
2018
2085
  `);
2019
2086
  let responseText = "";
2020
2087
  for await (const rawMsg of client.sendMessage({
2021
2088
  prompt,
2022
- sessionId: iteration > 0 ? lastSessionId : undefined,
2089
+ sessionId: lastSessionId ?? undefined,
2023
2090
  iteration: iteration + 1,
2024
2091
  ...opts.inbox ? { inboxStream: opts.inbox } : {}
2025
2092
  })) {
@@ -2035,7 +2102,15 @@ async function agentLoop(opts) {
2035
2102
  }
2036
2103
  const msg = outputVerdict.value;
2037
2104
  opts.onAgentEvent?.(iteration, msg);
2038
- opts.onAgentLifecycleEvent?.({ event: "agent.message", at: Date.now(), agentId, label, iteration: iteration + 1, message: summarizeAgentMessage(msg) });
2105
+ const summary = summarizeAgentMessage(msg);
2106
+ opts.onAgentLifecycleEvent?.({
2107
+ event: "agent.message",
2108
+ at: Date.now(),
2109
+ agentId,
2110
+ label,
2111
+ iteration: iteration + 1,
2112
+ message: summary.type === "usage" && client.model != null ? { ...summary, model: client.model } : summary
2113
+ });
2039
2114
  if (msg.type === "init")
2040
2115
  lastSessionId = msg.sessionId;
2041
2116
  if (msg.type === "text")
@@ -2078,6 +2153,8 @@ async function agentLoop(opts) {
2078
2153
  `);
2079
2154
  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
2155
  opts.onIteration?.(iteration + 1, status);
2156
+ if (schemaRetriesLeft-- > 0)
2157
+ iteration--;
2081
2158
  continue;
2082
2159
  }
2083
2160
  const parsed = validateResponse({ ...status, ...rawResponse });
@@ -2087,6 +2164,8 @@ raw: ${JSON.stringify(rawResponse).slice(0, 400)}
2087
2164
  `);
2088
2165
  status = { ...status, exit_signal: false, blockers: [`<response> schema validation failed: ${parsed.error.message}`] };
2089
2166
  opts.onIteration?.(iteration + 1, status);
2167
+ if (schemaRetriesLeft-- > 0)
2168
+ iteration--;
2090
2169
  continue;
2091
2170
  }
2092
2171
  response = parsed.data;
@@ -2107,9 +2186,16 @@ raw: ${JSON.stringify(rawResponse).slice(0, 400)}
2107
2186
  blockerStreak = null;
2108
2187
  continue;
2109
2188
  }
2110
- if (!status) {
2189
+ if (!status && rawResponse === null) {
2111
2190
  if (++iterationsWithoutStatus >= STALL_ITERATIONS)
2112
2191
  throw new Error(`${logLabel} stalled: no <status> block after ${iterationsWithoutStatus} iterations`);
2192
+ if (schemaRetriesLeft-- > 0) {
2193
+ lastResponseValidationError = "Your turn ended with no <status> block" + (opts.responseSchema ? " (and no <response> block)" : "") + ". " + "If you launched a background job (`… &`) and are waiting to be notified when it finishes — STOP: nothing will notify you here. " + "Poll it NOW (read its output / wait for it synchronously to completion), then emit your <status>" + (opts.responseSchema ? " and the complete <response> JSON." : ".");
2194
+ process.stdout.write(`${logLabel} empty turn (no <status>) — corrective re-prompt (${schemaRetriesLeft} retries left)
2195
+ `);
2196
+ iteration--;
2197
+ continue;
2198
+ }
2113
2199
  } else {
2114
2200
  iterationsWithoutStatus = 0;
2115
2201
  }
@@ -2124,6 +2210,12 @@ raw: ${JSON.stringify(rawResponse).slice(0, 400)}
2124
2210
  } else {
2125
2211
  blockerStreak = null;
2126
2212
  }
2213
+ if (opts.responseSchema && rawResponse !== null && lastResponseValidationError !== "" && status?.exit_signal !== false && schemaRetriesLeft-- > 0) {
2214
+ process.stdout.write(`${logLabel} response contract not yet satisfied — corrective re-prompt (${schemaRetriesLeft} retries left)
2215
+ `);
2216
+ iteration--;
2217
+ continue;
2218
+ }
2127
2219
  if (iteration + 1 < maxIterations)
2128
2220
  process.stdout.write(`${logLabel} continuing to iteration ${iteration + 2}/${maxIterations}
2129
2221
  `);
@@ -2249,14 +2341,34 @@ function translateMessage(message) {
2249
2341
  numTurns: Number(message.num_turns ?? 0),
2250
2342
  timestamp: ts
2251
2343
  });
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 });
2344
+ if (message.subtype === "error_max_turns") {
2345
+ msgs.push({ type: "done", sessionId: String(message.session_id ?? ""), timestamp: ts });
2346
+ } else if (message.is_error || message.subtype === "error_during_execution") {
2347
+ const detail = message.error ?? message.result ?? message.message;
2348
+ const text = detail !== undefined ? formatError(detail) : `${String(message.subtype ?? "unknown_error")} — raw result: ${JSON.stringify({ ...message, usage: undefined }).slice(0, 600)}`;
2349
+ msgs.push({ type: "error", text, timestamp: ts });
2254
2350
  } else {
2255
2351
  msgs.push({ type: "done", sessionId: String(message.session_id ?? ""), timestamp: ts });
2256
2352
  }
2257
2353
  }
2258
2354
  return msgs;
2259
2355
  }
2356
+ var MODEL_TIERS = {
2357
+ fable: "claude-fable-5",
2358
+ opus: "claude-opus-4-8",
2359
+ sonnet: "claude-sonnet-4-6",
2360
+ haiku: "claude-haiku-4-5"
2361
+ };
2362
+ function resolveClaudeModel(model) {
2363
+ if (!model)
2364
+ return DEFAULT_CLAUDE_MODEL;
2365
+ return MODEL_TIERS[model.toLowerCase()] ?? model;
2366
+ }
2367
+ function resolveThinking(model, thinking) {
2368
+ if (model.startsWith("claude-fable") && thinking && thinking.type === "disabled")
2369
+ return;
2370
+ return thinking;
2371
+ }
2260
2372
  var DEFAULT_CLAUDE_PATH = "/usr/local/bin/claude";
2261
2373
 
2262
2374
  class ClaudeRunner {
@@ -2269,7 +2381,7 @@ class ClaudeRunner {
2269
2381
  this.config = config;
2270
2382
  }
2271
2383
  get model() {
2272
- return this.config.model ?? this.options.model ?? DEFAULT_CLAUDE_MODEL;
2384
+ return resolveClaudeModel(this.config.model ?? this.options.model);
2273
2385
  }
2274
2386
  async gateToolCall(call, ctx) {
2275
2387
  const verdict = await runProcessorChain(this.options.processors ?? [], (p) => p.processToolCall, call, ctx);
@@ -2337,14 +2449,15 @@ class ClaudeRunner {
2337
2449
  tools: this.options.allowedTools,
2338
2450
  allowedTools: this.options.allowedTools,
2339
2451
  maxTurns: this.options.maxTurns,
2340
- model: this.config.model ?? this.options.model ?? DEFAULT_CLAUDE_MODEL,
2452
+ model: this.model,
2341
2453
  outputFormat: this.options.outputFormat,
2342
- thinking: this.config.thinking,
2454
+ thinking: resolveThinking(this.model, this.config.thinking),
2343
2455
  effort: this.config.effort,
2344
2456
  cwd: this.options.cwd,
2345
2457
  env: { ...process.env, ...this.config.env ?? {} },
2346
2458
  pathToClaudeCodeExecutable: this.config.pathToClaudeCodeExecutable ?? process.env.CLAUDE_CODE_EXECUTABLE ?? DEFAULT_CLAUDE_PATH,
2347
2459
  ...this.config.claudeMdContent ? { systemPrompt: { type: "preset", preset: "claude_code", append: this.config.claudeMdContent } } : {},
2460
+ skills: this.config.skills ?? "all",
2348
2461
  resume: opts.sessionId,
2349
2462
  mcpServers: this.config.mcpServers,
2350
2463
  permissionMode: "acceptEdits",
@@ -2364,7 +2477,12 @@ class ClaudeRunner {
2364
2477
  inboxQueue.close();
2365
2478
  }
2366
2479
  } catch (err) {
2367
- yield { type: "error", text: formatError(err), timestamp: now2() };
2480
+ const text = formatError(err);
2481
+ if (/maximum number of turns/i.test(text)) {
2482
+ yield { type: "done", sessionId: opts.sessionId ?? "", timestamp: now2() };
2483
+ } else {
2484
+ yield { type: "error", text, timestamp: now2() };
2485
+ }
2368
2486
  } finally {
2369
2487
  inboxQueue?.close();
2370
2488
  }
@@ -2633,11 +2751,34 @@ var amp_default = createAmpRuntime();
2633
2751
  import { promises as fs2 } from "node:fs";
2634
2752
  import { dirname as dirname2 } from "node:path";
2635
2753
  import { spawn } from "node:child_process";
2636
- import { Sandbox } from "e2b";
2754
+ import { Sandbox, SandboxNotFoundError, RateLimitError } from "e2b";
2637
2755
  import { Sandbox as Desktop } from "@e2b/desktop";
2638
2756
  import pRetry from "p-retry";
2757
+
2758
+ // src/sandbox-errors.ts
2759
+ var SANDBOX_UNAVAILABLE_PREFIX = "[sandbox-unavailable";
2760
+
2761
+ class SandboxUnavailableError extends Error {
2762
+ retryable;
2763
+ sandboxId;
2764
+ constructor(detail, opts) {
2765
+ super(`${SANDBOX_UNAVAILABLE_PREFIX}:${opts.retryable ? "retryable" : "terminal"}] ${detail}`);
2766
+ this.name = "SandboxUnavailableError";
2767
+ this.retryable = opts.retryable;
2768
+ this.sandboxId = opts.sandboxId;
2769
+ }
2770
+ }
2771
+
2772
+ // src/sandbox.ts
2639
2773
  var AGENT_COMPOSE_TAG = process.env.AGENT_COMPOSE_TAG ?? `agent-compose-${process.env.AGENT_COMPOSE_ENV ?? "dev"}`;
2640
2774
  var VERCEL_VM_LIFETIME_WINDOW_MS = 6 * 60 * 60 * 1000;
2775
+ var SANDBOX_VCPUS = {
2776
+ "2vcpu-4gb": 2,
2777
+ "4vcpu-8gb": 4,
2778
+ "8vcpu-16gb": 8,
2779
+ "32vcpu-64gb": 32
2780
+ };
2781
+ var DEFAULT_SANDBOX_SIZE = "2vcpu-4gb";
2641
2782
  function makeSandboxProvider(sb) {
2642
2783
  const commands = sb.commands;
2643
2784
  return {
@@ -2667,6 +2808,33 @@ function makeSandboxProvider(sb) {
2667
2808
  kill: () => sb.kill()
2668
2809
  };
2669
2810
  }
2811
+ function e2bMaxSandboxMs() {
2812
+ return Number(process.env.E2B_MAX_SANDBOX_MS) || 60 * 60 * 1000;
2813
+ }
2814
+ function isTransientSandboxError(error) {
2815
+ if (error instanceof SandboxNotFoundError || error instanceof RateLimitError)
2816
+ return true;
2817
+ const message = error instanceof Error ? error.message : String(error ?? "");
2818
+ 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);
2819
+ }
2820
+ function withSandboxRetry(fn, shouldRetry = isTransientSandboxError) {
2821
+ return pRetry(fn, { retries: 4, minTimeout: 400, factor: 2, shouldRetry });
2822
+ }
2823
+ function withSnapshotRetry(p) {
2824
+ if (!p.snapshot)
2825
+ return p;
2826
+ const snapshot = p.snapshot.bind(p);
2827
+ return { ...p, snapshot: () => withSandboxRetry(snapshot) };
2828
+ }
2829
+ function makeE2bSandboxProvider(sb) {
2830
+ return {
2831
+ ...makeSandboxProvider(sb),
2832
+ async snapshot() {
2833
+ const { snapshotId } = await sb.createSnapshot();
2834
+ return { snapshotId };
2835
+ }
2836
+ };
2837
+ }
2670
2838
  function makeDesktopSandboxProvider(sb) {
2671
2839
  return {
2672
2840
  ...makeSandboxProvider(sb),
@@ -2724,43 +2892,104 @@ async function parseSseExecStream(body, opts) {
2724
2892
  throw new Error("Sandbox exec stream ended without an exit event");
2725
2893
  return { exitCode, stdout, stderr };
2726
2894
  }
2895
+ var DOT_SEGMENT_PATH_RE2 = ".*(?:^|/|%2[fF])(?:\\.|%2[eE]){2}(?:/|%2[fF]|$).*";
2896
+ var DOT_SEGMENT_RE = /(?:^|\/|%2f)(?:\.|%2e){2}(?:\/|%2f|$)/i;
2897
+ function assertSafePathPrefix(domain, prefix) {
2898
+ if (!prefix.startsWith("/") || DOT_SEGMENT_RE.test(prefix)) {
2899
+ throw new Error(`Invalid Tier-2 pathPrefix ${JSON.stringify(prefix)} for "${domain}": prefixes must start with "/" and contain no dot-segments`);
2900
+ }
2901
+ }
2902
+ function toVercelNetworkPolicy(policy) {
2903
+ if (typeof policy === "string" || !policy.allow || Array.isArray(policy.allow)) {
2904
+ return policy;
2905
+ }
2906
+ const allow = {};
2907
+ for (const [domain, rules] of Object.entries(policy.allow)) {
2908
+ const translated = rules.flatMap((rule) => {
2909
+ const { requestRules, ...rest } = rule;
2910
+ const methods = requestRules?.methods ?? [];
2911
+ const prefixes = requestRules?.pathPrefixes ?? [];
2912
+ if (methods.length === 0 && prefixes.length === 0)
2913
+ return [rest];
2914
+ const methodMatch = methods.length > 0 ? { method: methods } : {};
2915
+ if (prefixes.length === 0)
2916
+ return [{ ...rest, match: methodMatch }];
2917
+ for (const prefix of prefixes)
2918
+ assertSafePathPrefix(domain, prefix);
2919
+ return prefixes.map((prefix) => ({ ...rest, match: { ...methodMatch, path: { startsWith: prefix } } }));
2920
+ });
2921
+ const hasPathGate = rules.some((rule) => (rule.requestRules?.pathPrefixes?.length ?? 0) > 0);
2922
+ allow[domain] = hasPathGate ? [{ match: { path: { regex: DOT_SEGMENT_PATH_RE2 } } }, ...translated] : translated;
2923
+ }
2924
+ return { ...policy, allow };
2925
+ }
2926
+ var VERCEL_MAX_TAGS = 5;
2927
+ function buildVercelTags(metadata) {
2928
+ const tags = { ...metadata, executor: AGENT_COMPOSE_TAG };
2929
+ const extras = Object.keys(tags).filter((k) => k !== "executor").sort();
2930
+ for (const key of extras.slice(VERCEL_MAX_TAGS - 1))
2931
+ delete tags[key];
2932
+ return tags;
2933
+ }
2727
2934
  function makeVercelSandboxProvider(sb, globalEnvs) {
2728
2935
  const mergeEnvs = (cmdEnvs) => globalEnvs ? { ...globalEnvs, ...cmdEnvs } : cmdEnvs;
2936
+ const asUnavailable = (err, retryable) => {
2937
+ throw new SandboxUnavailableError(err instanceof Error ? err.message : String(err), { retryable, sandboxId: sb.name });
2938
+ };
2729
2939
  return {
2730
- sandboxId: sb.sandboxId,
2940
+ sandboxId: sb.name,
2731
2941
  commands: {
2732
2942
  async run(cmd, opts) {
2733
2943
  const signal = opts?.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined;
2734
2944
  let stdout = "";
2735
2945
  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);
2946
+ 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));
2947
+ const collect = async () => {
2948
+ await pRetry(async (attempt) => {
2949
+ const h = attempt === 1 ? handle : await sb.getCommand(handle.cmdId);
2950
+ if (attempt > 1) {
2951
+ stdout = "";
2952
+ stderr = "";
2750
2953
  }
2751
- }
2752
- }, { retries: 3, minTimeout: 1000, factor: 2 });
2753
- const finished = await handle.wait();
2754
- return { exitCode: finished.exitCode, stdout, stderr };
2954
+ for await (const log of h.logs()) {
2955
+ if (log.stream === "stdout") {
2956
+ stdout += log.data;
2957
+ opts?.onStdout?.(log.data);
2958
+ } else {
2959
+ stderr += log.data;
2960
+ opts?.onStderr?.(log.data);
2961
+ }
2962
+ }
2963
+ }, { retries: 3, minTimeout: 1000, factor: 2 });
2964
+ const finished = await handle.wait();
2965
+ return { exitCode: finished.exitCode, stdout, stderr };
2966
+ };
2967
+ let timer;
2968
+ const deadline = opts?.timeoutMs ? new Promise((_, reject) => {
2969
+ timer = setTimeout(() => reject(new Error(`command timed out after ${opts.timeoutMs}ms: ${cmd.slice(0, 200)}`)), opts.timeoutMs);
2970
+ }) : undefined;
2971
+ try {
2972
+ return await (deadline ? Promise.race([collect(), deadline]) : collect());
2973
+ } catch (err) {
2974
+ if (err instanceof Error && err.message.startsWith("command timed out after"))
2975
+ throw err;
2976
+ if (signal?.aborted)
2977
+ throw new Error(`command timed out after ${opts?.timeoutMs}ms: ${cmd.slice(0, 200)}`);
2978
+ return asUnavailable(err, false);
2979
+ } finally {
2980
+ if (timer)
2981
+ clearTimeout(timer);
2982
+ }
2755
2983
  }
2756
2984
  },
2757
2985
  files: {
2758
2986
  async write(path, content) {
2759
- await sb.writeFiles([{ path, content }]);
2987
+ await sb.writeFiles([{ path, content }]).catch((err) => asUnavailable(err, true));
2760
2988
  }
2761
2989
  },
2762
2990
  async kill() {
2763
2991
  await sb.stop();
2992
+ await sb.delete();
2764
2993
  },
2765
2994
  async snapshot() {
2766
2995
  const res = await sb.snapshot({ expiration: 0 });
@@ -2769,6 +2998,9 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
2769
2998
  snapshotId: res.snapshotId,
2770
2999
  ...sizeBytes !== undefined ? { sizeBytes } : {}
2771
3000
  };
3001
+ },
3002
+ async updateNetworkPolicy(policy) {
3003
+ await sb.update({ networkPolicy: toVercelNetworkPolicy(policy) });
2772
3004
  }
2773
3005
  };
2774
3006
  }
@@ -2820,8 +3052,11 @@ var SANDBOX_PROVIDERS = {
2820
3052
  create: async (opts, env) => {
2821
3053
  const { Sandbox: VercelSandbox } = await import("@vercel/sandbox");
2822
3054
  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 });
3055
+ const np = opts.networkPolicy ? { networkPolicy: toVercelNetworkPolicy(opts.networkPolicy) } : {};
3056
+ const tags = buildVercelTags(opts.metadata);
3057
+ const tmpl = opts.template ?? process.env.VERCEL_DEFAULT_SNAPSHOT;
3058
+ const resources = { vcpus: SANDBOX_VCPUS[opts.size ?? DEFAULT_SANDBOX_SIZE] };
3059
+ const sb = await VercelSandbox.create(tmpl ? { source: { type: "snapshot", snapshotId: tmpl }, timeout: opts.timeoutMs, env: opts.envs, tags, persistent: false, resources, ...np, ...creds } : { runtime: "node24", timeout: opts.timeoutMs, env: opts.envs, tags, persistent: false, resources, ...np, ...creds });
2825
3060
  return makeVercelSandboxProvider(sb, opts.envs);
2826
3061
  },
2827
3062
  reconnect: async (sandboxId) => {
@@ -2831,7 +3066,10 @@ var SANDBOX_PROVIDERS = {
2831
3066
  teamId: process.env.VERCEL_TEAM_ID ?? "",
2832
3067
  projectId: process.env.VERCEL_PROJECT_ID ?? ""
2833
3068
  };
2834
- return makeVercelSandboxProvider(await VercelSandbox.get({ sandboxId, ...creds }));
3069
+ const sb = await VercelSandbox.get({ name: sandboxId, ...creds }).catch((err) => {
3070
+ throw new SandboxUnavailableError(err instanceof Error ? err.message : String(err), { retryable: true, sandboxId });
3071
+ });
3072
+ return makeVercelSandboxProvider(sb);
2835
3073
  },
2836
3074
  getActiveCount: async (env) => (await SANDBOX_PROVIDERS.vercel.listOwned(env)).length,
2837
3075
  listOwned: async (env) => {
@@ -2839,18 +3077,21 @@ var SANDBOX_PROVIDERS = {
2839
3077
  const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
2840
3078
  const since = Date.now() - VERCEL_VM_LIFETIME_WINDOW_MS;
2841
3079
  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)
3080
+ let cursor;
3081
+ for (let page = 0;page < 200; page++) {
3082
+ 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 });
3083
+ for (const sb of result.sandboxes) {
3084
+ if (sb.createdAt < since)
3085
+ return out;
2846
3086
  if (sb.status === "running") {
2847
- out.push({ sandboxId: sb.id, createdAt: new Date(sb.createdAt), metadata: {} });
3087
+ out.push({ sandboxId: sb.name, createdAt: new Date(sb.createdAt), metadata: sb.tags ?? {} });
2848
3088
  }
2849
- if (json.pagination.next === null)
3089
+ }
3090
+ if (result.pagination.next === null)
2850
3091
  return out;
2851
- until = json.pagination.next;
3092
+ cursor = result.pagination.next;
2852
3093
  }
2853
- throw new Error("Vercel listSandboxes exceeded 50 pages within the lifetime window — refuse to silently truncate");
3094
+ throw new Error("Vercel listSandboxes exceeded 200 pages (10k sandboxes) within the lifetime window — refuse to silently truncate");
2854
3095
  },
2855
3096
  deleteSnapshot: async (snapshotId, env) => {
2856
3097
  const { Snapshot } = await import("@vercel/sandbox");
@@ -2861,12 +3102,16 @@ var SANDBOX_PROVIDERS = {
2861
3102
  },
2862
3103
  e2b: {
2863
3104
  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));
3105
+ create: async ({ template, timeoutMs, networkPolicy: _np, size: _size, ...rest }) => {
3106
+ const maxMs = e2bMaxSandboxMs();
3107
+ if (timeoutMs > maxMs) {
3108
+ 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.`);
3109
+ }
3110
+ const sandboxOpts = { ...rest, timeoutMs: Math.min(timeoutMs, maxMs) };
3111
+ const tmpl = template ?? process.env.E2B_DEFAULT_TEMPLATE;
3112
+ return makeE2bSandboxProvider(await (tmpl ? Sandbox.create(tmpl, sandboxOpts) : Sandbox.create(sandboxOpts)));
2868
3113
  },
2869
- reconnect: async (sandboxId) => makeSandboxProvider(await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY ?? "", timeoutMs: 3600000 })),
3114
+ reconnect: async (sandboxId) => makeE2bSandboxProvider(await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY ?? "", timeoutMs: 3600000 })),
2870
3115
  killAll: async () => {
2871
3116
  const paginator = Sandbox.list({ query: { metadata: { executor: AGENT_COMPOSE_TAG } } });
2872
3117
  const sandboxes = [];
@@ -2892,14 +3137,17 @@ var SANDBOX_PROVIDERS = {
2892
3137
  }
2893
3138
  }
2894
3139
  return out;
3140
+ },
3141
+ deleteSnapshot: async (snapshotId, env) => {
3142
+ await Sandbox.deleteSnapshot(snapshotId, { apiKey: env.E2B_API_KEY });
2895
3143
  }
2896
3144
  },
2897
3145
  "e2b-desktop": {
2898
3146
  requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
2899
- create: async ({ template, ...sandboxOpts }) => {
3147
+ create: async ({ template, timeoutMs, networkPolicy: _np, size: _size, ...rest }) => {
2900
3148
  if (!template)
2901
3149
  throw new Error("E2B Desktop provider requires an explicit `template` (Dockerfile-based — no default base image)");
2902
- return makeDesktopSandboxProvider(await Desktop.create(template, sandboxOpts));
3150
+ return makeDesktopSandboxProvider(await Desktop.create(template, { ...rest, timeoutMs: Math.min(timeoutMs, e2bMaxSandboxMs()) }));
2903
3151
  }
2904
3152
  }
2905
3153
  };
@@ -2912,13 +3160,14 @@ async function createSandbox(provider, opts) {
2912
3160
  throw new Error(`Sandbox provider "${provider}" requires env vars:
2913
3161
  ${missing.join(`
2914
3162
  `)}`);
2915
- return def.create(opts, Object.fromEntries(Object.keys(def.requiredEnv).map((k) => [k, process.env[k]])));
3163
+ const env = Object.fromEntries(Object.keys(def.requiredEnv).map((k) => [k, process.env[k]]));
3164
+ return withSnapshotRetry(await def.create(opts, env));
2916
3165
  }
2917
3166
  async function reconnectSandbox(provider, sandboxId) {
2918
3167
  const def = SANDBOX_PROVIDERS[provider];
2919
3168
  if (!def?.reconnect)
2920
3169
  throw new Error(`Provider "${provider}" does not support reconnect`);
2921
- return def.reconnect(sandboxId);
3170
+ return withSnapshotRetry(await withSandboxRetry(() => def.reconnect(sandboxId), (error) => error instanceof SandboxNotFoundError ? error.attemptNumber <= 1 : isTransientSandboxError(error)));
2922
3171
  }
2923
3172
  async function deleteSandboxSnapshot(provider, snapshotId) {
2924
3173
  const def = SANDBOX_PROVIDERS[provider];
@@ -3079,14 +3328,6 @@ function scopedCheckpoint(scope) {
3079
3328
  // src/pause/wrappers.ts
3080
3329
  function buildPauseWrappers(pause) {
3081
3330
  return {
3082
- requestDecision(req) {
3083
- return pause({
3084
- reason: req.reason,
3085
- schema: req.schema,
3086
- ...req.payload !== undefined ? { payload: req.payload } : {},
3087
- ...req.ttlMs !== undefined ? { ttlMs: req.ttlMs } : {}
3088
- }, "decision");
3089
- },
3090
3331
  sleep(durationMs) {
3091
3332
  return pause({
3092
3333
  reason: `sleep ${durationMs}ms`,
@@ -3396,7 +3637,7 @@ function buildInvokeChild(runId, opts = {}) {
3396
3637
  return (name, input, childOpts) => getChildClient().invokeAndWait(name, input, {
3397
3638
  ...childOpts,
3398
3639
  parentRunId: runId,
3399
- factorySlug: childOpts?.factorySlug ?? opts.defaultFactorySlug ?? process.env.AGENT_COMPOSE_FACTORY_SLUG ?? "default"
3640
+ factorySlug: childOpts?.factorySlug ?? opts.defaultFactorySlug ?? process.env.AGENT_COMPOSE_FACTORY ?? process.env.AGENT_COMPOSE_FACTORY_SLUG ?? "default"
3400
3641
  });
3401
3642
  }
3402
3643
  // src/workflow-steps/step.ts
@@ -3436,6 +3677,9 @@ function stepInputPath(stepIndex) {
3436
3677
  function requestContextPath(stepIndex) {
3437
3678
  return `/tmp/wf/request-context-${stepIndex}.json`;
3438
3679
  }
3680
+ function stepResultFilePath(token) {
3681
+ return `/tmp/wf/step-result-${token}.json`;
3682
+ }
3439
3683
  // src/step-invocation/invoker.ts
3440
3684
  import { randomBytes as randomBytes2 } from "node:crypto";
3441
3685
  import { z as z10 } from "zod";
@@ -3491,6 +3735,18 @@ function parseStepResult(stdout, resultToken) {
3491
3735
  if (pauseLine && resultLine)
3492
3736
  break;
3493
3737
  }
3738
+ const midLine = (prefix) => {
3739
+ const at = stdout.indexOf(prefix);
3740
+ if (at === -1)
3741
+ return;
3742
+ const end = stdout.indexOf(`
3743
+ `, at);
3744
+ return stdout.slice(at, end === -1 ? undefined : end);
3745
+ };
3746
+ if (!pauseLine)
3747
+ pauseLine = midLine(pausePrefix);
3748
+ if (!resultLine)
3749
+ resultLine = midLine(resultPrefix);
3494
3750
  if (pauseLine) {
3495
3751
  try {
3496
3752
  const raw = JSON.parse(pauseLine.slice(pausePrefix.length));
@@ -3578,17 +3834,35 @@ async function invokeStep(sandbox, request, opts) {
3578
3834
  };
3579
3835
  const stdoutSplitter = makeLineSplitter(opts?.onStdout, true);
3580
3836
  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
- });
3837
+ let result;
3838
+ try {
3839
+ result = await sandbox.commands.run(RUNNER_COMMAND, {
3840
+ envs,
3841
+ timeoutMs: 0,
3842
+ ...stdoutSplitter.onChunk ? { onStdout: stdoutSplitter.onChunk } : {},
3843
+ ...stderrSplitter.onChunk ? { onStderr: stderrSplitter.onChunk } : {}
3844
+ });
3845
+ } catch (e) {
3846
+ if (e instanceof SandboxUnavailableError)
3847
+ throw e;
3848
+ const ce = e;
3849
+ result = {
3850
+ stdout: ce.stdout ?? ce.result?.stdout ?? "",
3851
+ stderr: ce.stderr ?? ce.result?.stderr ?? (e instanceof Error ? e.message : String(e)),
3852
+ exitCode: ce.exitCode ?? ce.result?.exitCode ?? 1
3853
+ };
3854
+ }
3587
3855
  stdoutSplitter.flush();
3588
3856
  stderrSplitter.flush();
3589
3857
  const parsed = parseStepResult(result.stdout, resultToken);
3590
3858
  if (parsed)
3591
3859
  return parsed;
3860
+ const fileRead = await sandbox.commands.run(`cat ${stepResultFilePath(resultToken)} 2>/dev/null || true`).catch(() => null);
3861
+ if (fileRead?.stdout) {
3862
+ const fromFile = parseStepResult(fileRead.stdout, resultToken);
3863
+ if (fromFile)
3864
+ return fromFile;
3865
+ }
3592
3866
  if (result.exitCode !== 0) {
3593
3867
  const stderrTail = result.stderr.trim().slice(-2000);
3594
3868
  const stdoutTail = result.stdout.trim().slice(-2000);
@@ -3609,13 +3883,19 @@ ${details}` : ""}`,
3609
3883
  }
3610
3884
  };
3611
3885
  }
3886
+ const tail = result.stdout.trim().slice(-500);
3612
3887
  return {
3613
3888
  ok: false,
3614
- error: { kind: "protocol", message: "no tokenised step result found on stdout" }
3889
+ error: {
3890
+ kind: "protocol",
3891
+ message: `no tokenised step result on stdout or in the result file (runner exited 0 without emitting)${tail ? `
3892
+ stdout tail:
3893
+ ${tail}` : ""}`
3894
+ }
3615
3895
  };
3616
3896
  }
3617
3897
  // src/step-invocation/server.ts
3618
- import { readFileSync } from "node:fs";
3898
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3619
3899
  function writeSentinelLine(line) {
3620
3900
  return new Promise((resolve, reject) => {
3621
3901
  process.stdout.write(line, (err) => {
@@ -3626,9 +3906,21 @@ function writeSentinelLine(line) {
3626
3906
  });
3627
3907
  });
3628
3908
  }
3629
- function emitResult(token, payload) {
3630
- return writeSentinelLine(`${STEP_RESULT_PREFIX}${token}:${JSON.stringify(payload)}
3909
+ function persistSentinelFile(token, line) {
3910
+ try {
3911
+ mkdirSync("/tmp/wf", { recursive: true });
3912
+ writeFileSync(stepResultFilePath(token), line);
3913
+ } catch (err) {
3914
+ process.stderr.write(`[step-server] result-file persist failed: ${err instanceof Error ? err.message : String(err)}
3631
3915
  `);
3916
+ }
3917
+ }
3918
+ function emitResult(token, payload) {
3919
+ const line = `${STEP_RESULT_PREFIX}${token}:${JSON.stringify(payload)}
3920
+ `;
3921
+ persistSentinelFile(token, line);
3922
+ return writeSentinelLine(`
3923
+ ${line}`);
3632
3924
  }
3633
3925
  function emitPause(token, signal) {
3634
3926
  const body = JSON.stringify({
@@ -3636,8 +3928,11 @@ function emitPause(token, signal) {
3636
3928
  pauseRequest: signal.pauseRequest,
3637
3929
  ...signal.observability ? { observability: signal.observability } : {}
3638
3930
  });
3639
- return writeSentinelLine(`${STEP_PAUSE_PREFIX}${token}:${body}
3640
- `);
3931
+ const line = `${STEP_PAUSE_PREFIX}${token}:${body}
3932
+ `;
3933
+ persistSentinelFile(token, line);
3934
+ return writeSentinelLine(`
3935
+ ${line}`);
3641
3936
  }
3642
3937
  function scrubProtocolEnvs() {
3643
3938
  for (const key of [
@@ -3709,6 +4004,149 @@ async function serveStep(handler) {
3709
4004
  import { z as z11 } from "zod";
3710
4005
  import { randomUUID as randomUUID2 } from "node:crypto";
3711
4006
 
4007
+ // src/agent/agent-context.ts
4008
+ var AGENT_COMPOSE_MANUAL = `# Working inside an Agent Compose sandbox
4009
+
4010
+ You are an agent running in a per-run sandbox on the Agent Compose platform.
4011
+ Use the **\`agentc\` CLI** and the **\`@agent-compose/sdk\`** for everything below —
4012
+ do NOT hand-roll raw HTTP/curl calls against the platform API. The CLI is on
4013
+ your PATH and already authenticated from the environment
4014
+ (\`AGENT_COMPOSE_URL\` / \`AGENT_COMPOSE_API_KEY\` / \`AGENT_COMPOSE_FACTORY\` are
4015
+ injected for this run), so commands just work — no login, no keys to manage.
4016
+
4017
+ The \`/ac:*\` skills are installed as Claude Code slash commands (\`/ac:invoke\`,
4018
+ \`/ac:events\`, \`/ac:logs\`, \`/ac:register\`, …) — reach for them too.
4019
+
4020
+ ## Files — your outputs persist by default
4021
+
4022
+ Your working directory defaults to **\`$AGENT_COMPOSE_RUN_DIR\`** — a per-run
4023
+ directory on the shared factory drive
4024
+ (\`$AGENT_COMPOSE_FACTORY_DIR/<workflow>/<version>/<run-id>/\`) the platform
4025
+ creates and attributes to this run. **Files you write here persist by
4026
+ default** — they show up in the dashboard's Files tab and the run's Artifacts
4027
+ card, with no API calls to save them. The dir already exists and is writable.
4028
+
4029
+ Need throwaway scratch — heavy build output, package caches, temp files?
4030
+ \`cd /tmp\` (or any path outside \`/factory\`): anything off the factory drive is
4031
+ ephemeral and discarded when the sandbox ends. In short: **stay in your working
4032
+ dir to keep something, \`cd\` out to throw it away.**
4033
+
4034
+ The whole shared drive is POSIX-mounted at \`/factory\`; the dashboard-visible
4035
+ root is \`$AGENT_COMPOSE_FACTORY_DIR\` (\`/factory/files\`). Earlier versions and
4036
+ runs live in sibling dirs under
4037
+ \`$AGENT_COMPOSE_FACTORY_DIR/$AGENT_COMPOSE_WORKFLOW/\` — read them for prior
4038
+ context. Other workflows' dirs are present but not your concern.
4039
+
4040
+ ## Events — the factory timeline
4041
+
4042
+ Record something on the run/factory timeline (the dashboard renders these)
4043
+ with the CLI — your run id is \`$RUN_ID\`:
4044
+
4045
+ agentc events send "$RUN_ID" <name> --summary "<one line>" [--body '<json>']
4046
+
4047
+ Names like \`note.created\` / \`brief.posted\` surface in the Workbench;
4048
+ \`agentc events list\` reads them back. \`/ac:events\` is the skill equivalent.
4049
+
4050
+ ## Runs
4051
+
4052
+ agentc list # registered workflows (/ac:list)
4053
+ agentc logs "$RUN_ID" # a run's logs (/ac:logs)
4054
+ agentc invoke <workflow> -i '<json>' # dispatch a workflow (/ac:invoke)
4055
+
4056
+ ## Writing workflow / agent code — the SDK
4057
+
4058
+ \`@agent-compose/sdk\` is installed in \`/workspace\` — import it from any script
4059
+ you write there:
4060
+
4061
+ import { defineWorkflow, agent, AgentComposeClient } from "@agent-compose/sdk";
4062
+
4063
+ Use \`/ac:generate-workflow\` / \`/ac:generate-agent\` to scaffold, then
4064
+ \`agentc register <file.ts>\` (or \`/ac:register\`).
4065
+
4066
+ ## Pausing to ask the human — \`agentc pause\`
4067
+
4068
+ When you can't or shouldn't proceed without a human, run \`agentc pause\`. It
4069
+ blocks until they answer on the dashboard, then prints their answer to stdout:
4070
+
4071
+ ANSWER=$(agentc pause --reason "Notion returned 401 — connect Notion to continue" \\
4072
+ --option retry --option skip)
4073
+
4074
+ Reach for it the moment you hit — or foresee — any of these:
4075
+ - **A wall only a human can clear:** a 401/403, a missing credential, an
4076
+ unconnected provider, a host the network refuses. Do NOT retry blindly or try
4077
+ to work around it — pause and say what needs enabling.
4078
+ - **A durable or outward-facing action that needs sign-off:** registering a
4079
+ workflow, deploying, sending email/messages, deleting or overwriting shared
4080
+ data, spending money. Prepare everything, then pause for approval BEFORE you
4081
+ commit it.
4082
+ - **A judgment call only the human can settle:** an under-specified request,
4083
+ several valid paths, a conflict with existing state, missing input only they have.
4084
+
4085
+ You compose the \`--reason\` (the ask) yourself; pass \`--option\` choices when
4086
+ there are clear ones, omit them for a free-form answer. Read the printed answer
4087
+ and act on it. Each agent pauses independently — pausing doesn't stop the others.
4088
+
4089
+ ## Credentials
4090
+
4091
+ Connector credentials (Google, GitHub, …) are NEVER in your environment.
4092
+ They're injected at the network layer when you call an allowed host — make the
4093
+ request **without** an Authorization header and the platform adds it. Don't try
4094
+ to read or exfiltrate tokens; they aren't here. The "Connectors & access"
4095
+ section below (when present) lists exactly which providers this run can reach.
4096
+
4097
+ ## Tools in this environment
4098
+
4099
+ - \`agentc\` — Agent Compose CLI (your primary interface; authed from env)
4100
+ - \`@agent-compose/sdk\` — installed in /workspace for writing workflows
4101
+ - \`/ac:*\` Claude Code skills — slash commands for the above
4102
+ - \`archil\` (factory drive), \`rtk\`, \`bun\`
4103
+ - A world-writable \`/workspace\` working directory`;
4104
+ function renderConnectorsSection(connectors) {
4105
+ if (connectors.length === 0)
4106
+ return "";
4107
+ const rows = connectors.map((c) => {
4108
+ const host = c.hosts?.length ? c.hosts.join(", ") : "(host set by the platform)";
4109
+ const verbs = c.methods?.length ? c.methods.join("/") : "any method";
4110
+ const paths = c.pathPrefixes?.length ? ` under ${c.pathPrefixes.join(", ")}` : "";
4111
+ const repo = c.repository ? ` — repo \`${c.repository}\` (${c.access ?? "read"})` : "";
4112
+ const why = c.scopes?.length ? `
4113
+ _scopes: ${c.scopes.join(", ")}_` : "";
4114
+ return `- **${c.name ?? c.provider}** → \`${host}\` — ${verbs}${paths}${repo}${why}`;
4115
+ });
4116
+ return `
4117
+
4118
+ ## Connectors & access — what this run can reach
4119
+
4120
+ These providers are connected for this run. Call their APIs with plain
4121
+ fetch/SDKs and **no Authorization header** — the platform injects the
4122
+ credential at the network layer. Requests outside the listed method/path are
4123
+ refused (403) and the token withheld. Anything NOT listed is unreachable; if
4124
+ you need it, \`agentc pause\` and ask for it to be connected.
4125
+
4126
+ ${rows.join(`
4127
+ `)}
4128
+ `;
4129
+ }
4130
+ function buildAgentContextDoc(env) {
4131
+ let connectors = [];
4132
+ const raw = env.AGENT_COMPOSE_CONNECTORS;
4133
+ if (raw) {
4134
+ try {
4135
+ const parsed = JSON.parse(raw);
4136
+ if (Array.isArray(parsed))
4137
+ connectors = parsed;
4138
+ } catch {}
4139
+ }
4140
+ return AGENT_COMPOSE_MANUAL + renderConnectorsSection(connectors);
4141
+ }
4142
+ async function writeAgentContext(args) {
4143
+ const doc = buildAgentContextDoc(args.env);
4144
+ const dir = args.cwd.replace(/\/+$/, "") || "/workspace";
4145
+ for (const name of ["AGENTS.md", "CLAUDE.md", "GEMINI.md"]) {
4146
+ await args.sandbox.files.write(`${dir}/${name}`, doc);
4147
+ }
4148
+ }
4149
+
3712
4150
  // src/agent/protocol-suffix.md
3713
4151
  var protocol_suffix_default = '## Status Signal\n\nWhen you have finished your work or are blocked, emit a `<status>` block at the end of your response:\n\n```json\n<status>\n{\n "summary": "one sentence describing what was done or what is blocking",\n "completed": ["each acceptance criterion that is now fully met"],\n "blockers": [],\n "changed_files": ["relative/path/to/file"],\n "tests_run": true,\n "exit_signal": true\n}\n</status>\n```\n\n**Field semantics:**\n- `summary`: one sentence — what was accomplished or what is blocking\n- `completed`: acceptance criteria items that are fully done — be specific\n- `blockers`: non-empty when `exit_signal: false` — describe the exact obstacle\n- `changed_files`: relative paths of files you created or modified\n- `tests_run`: `true` if you ran any test suite (pass or fail); `false` if no tests exist or you skipped them\n- `exit_signal: true` — set when ALL acceptance criteria are met and no blockers remain\n- `exit_signal: false` — set when blocked or unfinished; `blockers` must be non-empty\n\n**If you are still actively working** and have not reached a natural stopping point, do NOT emit a `<status>` block — just keep working.\n\n**Example (done):**\n\n```json\n<status>\n{\n "summary": "Added input validation middleware to /api/tasks with tests",\n "completed": ["POST /api/tasks validates required fields", "Returns 400 with details on invalid input", "Unit tests passing"],\n "blockers": [],\n "changed_files": ["src/middleware/validate.ts", "src/routes/tasks.ts", "tests/validate.test.ts"],\n "tests_run": true,\n "exit_signal": true\n}\n</status>\n```\n\n**Example (blocked):**\n\n```json\n<status>\n{\n "summary": "Implemented middleware but tests are failing due to module resolution",\n "completed": ["Middleware created and wired into route"],\n "blockers": ["Tests fail: cannot resolve import \'./validate\' — module resolution config unclear"],\n "changed_files": ["src/middleware/validate.ts"],\n "tests_run": true,\n "exit_signal": false\n}\n</status>\n```\n';
3714
4152
 
@@ -3824,7 +4262,28 @@ function resolveAgentId(explicitId) {
3824
4262
  return `step${activeCall.stepIndex}-agent-${activeCall.callIndex}`;
3825
4263
  }
3826
4264
  async function agent(opts) {
3827
- const workingDir = opts.workingDir ?? "";
4265
+ let workingDir = opts.workingDir || process.env.AGENT_COMPOSE_RUN_DIR || "/workspace";
4266
+ const CONTEXT_WRITE_DEADLINE_MS = 15000;
4267
+ const tryWriteContext = (cwd) => {
4268
+ let timer;
4269
+ return Promise.race([
4270
+ writeAgentContext({ sandbox: opts.sandbox, cwd, env: process.env }).then(() => true),
4271
+ new Promise((resolve) => {
4272
+ timer = setTimeout(() => resolve(false), CONTEXT_WRITE_DEADLINE_MS);
4273
+ })
4274
+ ]).catch((err) => {
4275
+ console.error(`[agent] writeAgentContext(${cwd}) failed: ${err instanceof Error ? err.message : String(err)}`);
4276
+ return false;
4277
+ }).finally(() => {
4278
+ if (timer)
4279
+ clearTimeout(timer);
4280
+ });
4281
+ };
4282
+ if (!await tryWriteContext(workingDir) && workingDir !== "/workspace") {
4283
+ console.error(`[agent] working dir ${workingDir} not writable within ${CONTEXT_WRITE_DEADLINE_MS}ms ` + `(factory drive degraded?) — falling back to /workspace so the agent can run`);
4284
+ workingDir = "/workspace";
4285
+ await tryWriteContext(workingDir);
4286
+ }
3828
4287
  const agentId = resolveAgentId(opts.agentId);
3829
4288
  const callbackUrl = process.env.AGENT_COMPOSE_URL;
3830
4289
  const callbackToken = process.env.AGENT_COMPOSE_RUN_TOKEN;
@@ -3908,6 +4367,7 @@ async function agent(opts) {
3908
4367
  }
3909
4368
  export {
3910
4369
  writeTool,
4370
+ writeAgentContext,
3911
4371
  stepInputPath,
3912
4372
  serveStep,
3913
4373
  runWorkflowSteps,
@@ -3955,6 +4415,7 @@ export {
3955
4415
  bundleWorkflow,
3956
4416
  buildStepEnvs,
3957
4417
  buildInvokeChild,
4418
+ buildAgentContextDoc,
3958
4419
  bashTool,
3959
4420
  assertDefaultExportIsDefineWorkflow,
3960
4421
  amp_default as ampRuntime,
@@ -3969,9 +4430,11 @@ export {
3969
4430
  TMP_DIR,
3970
4431
  StepValidationError,
3971
4432
  StepExecutionError,
4433
+ SandboxUnavailableError,
3972
4434
  STEP_RESULT_PREFIX,
3973
4435
  STEP_PAUSE_PREFIX,
3974
4436
  STEP_ENV,
4437
+ SANDBOX_UNAVAILABLE_PREFIX,
3975
4438
  ReservedKeyError,
3976
4439
  RequestContext,
3977
4440
  RUNNER_COMMAND,
@@ -3991,6 +4454,7 @@ export {
3991
4454
  AgentComposeError,
3992
4455
  AgentComposeClient,
3993
4456
  AGENT_COMPOSE_TAG,
4457
+ AGENT_COMPOSE_MANUAL,
3994
4458
  AC_WORKFLOW_ID,
3995
4459
  AC_TEAM_ID,
3996
4460
  AC_RUN_ID,