@agent-compose/sdk 0.5.7 → 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.
package/src/sandbox.ts CHANGED
@@ -71,6 +71,35 @@ export type SandboxNetworkPolicy =
71
71
  subnets?: SandboxNetworkSubnetPolicy;
72
72
  };
73
73
 
74
+ /** Sandbox machine size. A coarse small/medium/large knob that maps to
75
+ * provider machine specs at create time. Vercel honours it natively via
76
+ * `resources.vcpus` (2048 MB RAM per vCPU). E2B sizing is baked into the
77
+ * template, so E2B ignores this field. Default: "small". */
78
+ /** Sandbox hardware SKU. Named for the actual machine spec (vCPU + RAM) rather
79
+ * than abstract t-shirt sizes. Memory is always 2048 MB per vCPU:
80
+ * 2vcpu-4gb = 2 vCPU / 4 GiB (Vercel's own default machine)
81
+ * 4vcpu-8gb = 4 vCPU / 8 GiB
82
+ * 8vcpu-16gb = 8 vCPU / 16 GiB (per-sandbox ceiling on STANDARD accounts —
83
+ * probed live: 16 & 32 vCPU 400 on dev)
84
+ * 32vcpu-64gb = 32 vCPU / 64 GiB (ENTERPRISE ONLY — standard accounts reject >8 vCPU) */
85
+ export type SandboxSize = "2vcpu-4gb" | "4vcpu-8gb" | "8vcpu-16gb" | "32vcpu-64gb";
86
+
87
+ /** SKU → Vercel vCPU count (RAM follows at 2048 MB/vCPU). */
88
+ export const SANDBOX_VCPUS: Record<SandboxSize, number> = {
89
+ "2vcpu-4gb": 2,
90
+ "4vcpu-8gb": 4,
91
+ "8vcpu-16gb": 8,
92
+ "32vcpu-64gb": 32,
93
+ };
94
+
95
+ /** SDK fallback size when neither the caller nor the deployment specifies one.
96
+ * Deliberately conservative — the OPERATIONAL default is chosen per-environment
97
+ * by the server via the `SANDBOX_DEFAULT_SIZE` env var (prod = Enterprise →
98
+ * `32vcpu-64gb`; dev = `8vcpu-16gb`, since the dev account caps at 8 vCPU).
99
+ * TODO(sandbox-size): the prod default is temporarily `32vcpu-64gb` for a
100
+ * memory-hungry workload — lower it back when no longer needed. */
101
+ export const DEFAULT_SANDBOX_SIZE: SandboxSize = "2vcpu-4gb";
102
+
74
103
  export interface SandboxCreateOpts {
75
104
  envs: Record<string, string>;
76
105
  metadata: Record<string, string>;
@@ -78,6 +107,9 @@ export interface SandboxCreateOpts {
78
107
  /** Provider-specific template/snapshot identifier. E2B: template id or
79
108
  * snapshot id (omit → E2B's default base). Vercel: snapshot id (omit → node24). */
80
109
  template?: string;
110
+ /** Machine size. Vercel maps it to `resources.vcpus`; E2B ignores it
111
+ * (size is template-defined). Omit → `DEFAULT_SANDBOX_SIZE`. */
112
+ size?: SandboxSize;
81
113
  /** Outbound request policy with header transforms — ONE shape for every
82
114
  * provider; only the enforcement point differs. Vercel's firewall
83
115
  * enforces + injects natively from this value. E2B enforces it via an
@@ -417,16 +449,11 @@ function makeVercelSandboxProvider(sb: any, globalEnvs?: Record<string, string>)
417
449
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
418
450
  const handle: any = await sb.runCommand({ cmd: "sh", args: ["-c", cmd], cwd: opts?.cwd, env: mergeEnvs(opts?.envs), detached: true, signal, ...(opts?.sudo ? { sudo: true } : {}) })
419
451
  .catch((err: unknown) => asUnavailable(err, true));
420
- // Reconnect to the already-running command on transient stream failures (e.g. BrotliDecompressionError).
421
- // `h.logs()` replays from the start on reconnect, so reset accumulators per
422
- // attempt to avoid double-counting. The streaming callbacks may still fire
423
- // for duplicate chunks during retries — an acceptable tradeoff for resilience.
424
- //
425
- // A failure here (mid-stream / on `wait`) is NOT safe to retry: the
426
- // runner already launched, so user code may have produced side
427
- // effects. Surface it as a terminal `SandboxUnavailableError` so it's
428
- // classified honestly but not auto-replayed.
429
- try {
452
+ // Stream + wait. Reconnect to the already-running command on transient
453
+ // stream failures (e.g. BrotliDecompressionError). `h.logs()` replays
454
+ // from the start on reconnect, so reset accumulators per attempt to
455
+ // avoid double-counting.
456
+ const collect = async () => {
430
457
  await pRetry(async (attempt) => {
431
458
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
432
459
  const h: any = attempt === 1 ? handle : await sb.getCommand(handle.cmdId);
@@ -438,17 +465,34 @@ function makeVercelSandboxProvider(sb: any, globalEnvs?: Record<string, string>)
438
465
  }, { retries: 3, minTimeout: 1_000, factor: 2 });
439
466
  const finished = await handle.wait();
440
467
  return { exitCode: finished.exitCode, stdout, stderr };
468
+ };
469
+ // `timeoutMs` MUST be authoritative. The AbortSignal alone is not: a
470
+ // command that RUNS but emits nothing (e.g. a wedged `archil checkout`
471
+ // on a blocked data plane) leaves `logs()`/`wait()` pending and the
472
+ // abort never interrupts the await — the call hangs for the activity's
473
+ // whole multi-hour ceiling. Race a hard client-side deadline so a hung
474
+ // command fails fast and the caller's degrade/retry policy takes over.
475
+ let timer: ReturnType<typeof setTimeout> | undefined;
476
+ const deadline = opts?.timeoutMs
477
+ ? new Promise<never>((_, reject) => {
478
+ timer = setTimeout(
479
+ () => reject(new Error(`command timed out after ${opts.timeoutMs}ms: ${cmd.slice(0, 200)}`)),
480
+ opts.timeoutMs);
481
+ })
482
+ : undefined;
483
+ try {
484
+ return await (deadline ? Promise.race([collect(), deadline]) : collect());
441
485
  } catch (err) {
442
- // A caller-requested timeout (opts.timeoutMs → AbortSignal) is a
443
- // COMMAND timing out, not the sandbox dying — the VM is alive and
444
- // answering; one command overran its budget. Classifying it as
445
- // terminal sandbox-unavailable failed whole runs over a single
446
- // slow mount probe (observed live). Surface it as a plain error
447
- // so the caller's own retry/deadline policy decides.
448
- if (signal?.aborted) {
449
- throw new Error(`command timed out after ${opts?.timeoutMs}ms: ${cmd.slice(0, 200)}`);
450
- }
486
+ // A timeout (hard deadline or AbortSignal) is a COMMAND overrunning
487
+ // its budget, not the sandbox dying — the VM is alive. Surface it as
488
+ // a plain error so the caller's own retry/deadline policy decides
489
+ // (classifying it as terminal sandbox-unavailable failed whole runs
490
+ // over a single slow mount probe, observed live).
491
+ if (err instanceof Error && err.message.startsWith("command timed out after")) throw err;
492
+ if (signal?.aborted) throw new Error(`command timed out after ${opts?.timeoutMs}ms: ${cmd.slice(0, 200)}`);
451
493
  return asUnavailable(err, false);
494
+ } finally {
495
+ if (timer) clearTimeout(timer);
452
496
  }
453
497
  },
454
498
  },
@@ -567,14 +611,19 @@ const SANDBOX_PROVIDERS: Record<string, SandboxProviderDef> = {
567
611
  // bakes on top of this base chains from it — the base tools are in
568
612
  // every derived snapshot for free.
569
613
  const tmpl = opts.template ?? process.env.VERCEL_DEFAULT_SNAPSHOT;
614
+ // Machine size → vCPUs (RAM auto-follows at 2048 MB/vCPU). Always sent
615
+ // explicitly so the spec is deterministic and self-documenting rather
616
+ // than riding Vercel's implicit default; "small" maps to that default
617
+ // anyway, so existing runs are unchanged.
618
+ const resources = { vcpus: SANDBOX_VCPUS[opts.size ?? DEFAULT_SANDBOX_SIZE] };
570
619
  // `persistent: false` — @vercel/sandbox 2.x creates persistent-by-default
571
620
  // sandboxes: stop() auto-snapshots (and keeps billing storage), commands
572
621
  // transparently resume a stopped VM, and kill no longer destroys. Our
573
622
  // sandboxes are single-run and lifecycle-managed by the engine (explicit
574
623
  // snapshot() / kill()), so opt out in BOTH branches.
575
624
  const sb = await VercelSandbox.create(tmpl
576
- ? { source: { type: "snapshot" as const, snapshotId: tmpl }, timeout: opts.timeoutMs, env: opts.envs, tags, persistent: false, ...np, ...creds }
577
- : { runtime: "node24" as const, timeout: opts.timeoutMs, env: opts.envs, tags, persistent: false, ...np, ...creds },
625
+ ? { source: { type: "snapshot" as const, snapshotId: tmpl }, timeout: opts.timeoutMs, env: opts.envs, tags, persistent: false, resources, ...np, ...creds }
626
+ : { runtime: "node24" as const, timeout: opts.timeoutMs, env: opts.envs, tags, persistent: false, resources, ...np, ...creds },
578
627
  );
579
628
  // Pass envs as globalEnvs so they're injected into every runCommand subprocess.
580
629
  // (Vercel's Sandbox.create env parameter does not flow to runCommand subprocesses.)
@@ -649,7 +698,10 @@ const SANDBOX_PROVIDERS: Record<string, SandboxProviderDef> = {
649
698
  },
650
699
  "e2b": {
651
700
  requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
652
- create: async ({ template, timeoutMs, networkPolicy: _np, ...rest }) => {
701
+ // `size` dropped here: E2B has no create-time resource knob (specs are
702
+ // baked into the template/snapshot), so sizing on E2B = a pre-sized
703
+ // template, not this field. Honoured only on Vercel.
704
+ create: async ({ template, timeoutMs, networkPolicy: _np, size: _size, ...rest }) => {
653
705
  // `template` is an E2B template id, a snapshot id (a valid create source
654
706
  // that persists beyond its origin sandbox — bootFrom parity), or absent →
655
707
  // E2B's default base (Debian + node + npm/python/git), mirroring Vercel's
@@ -722,7 +774,10 @@ const SANDBOX_PROVIDERS: Record<string, SandboxProviderDef> = {
722
774
  // its transforms carry live connector tokens, and a credential-bearing
723
775
  // object must never cross into a third-party library's option bag.
724
776
  // Lifetime is clamped to E2B's per-plan cap the same way.
725
- create: async ({ template, timeoutMs, networkPolicy: _np, ...rest }) => {
777
+ // `size` dropped here: E2B has no create-time resource knob (specs are
778
+ // baked into the template/snapshot), so sizing on E2B = a pre-sized
779
+ // template, not this field. Honoured only on Vercel.
780
+ create: async ({ template, timeoutMs, networkPolicy: _np, size: _size, ...rest }) => {
726
781
  if (!template) throw new Error("E2B Desktop provider requires an explicit `template` (Dockerfile-based — no default base image)");
727
782
  return makeDesktopSandboxProvider(
728
783
  await Desktop.create(template, { ...rest, timeoutMs: Math.min(timeoutMs, e2bMaxSandboxMs()) }),
@@ -43,7 +43,7 @@ export type StepResult<TOutput = unknown> =
43
43
  * it so the runtime type and the parsed shape can't drift.
44
44
  *
45
45
  * Loose by design (the inner zod shape stops at orchestration fields
46
- * the engine needs): the SDK wrapper layer (`requestDecision` /
46
+ * the engine needs): the SDK wrapper layer (`sleep` /
47
47
  * `sleep` / `waitForEvent`) owns its own payload contract, and the
48
48
  * engine treats `payload` as opaque. */
49
49
  export const StepPauseRequestSchema = z.object({
@@ -3,7 +3,7 @@
3
3
  import type { InvokeAndWaitOptions, RunStatus } from "../client.js";
4
4
  import type { RequestContext } from "../request-context/request-context.js";
5
5
  import type { PauseRequest } from "../pause/pause-core.js";
6
- import type { RequestDecisionRequest, WaitForEventRequest } from "../pause/wrappers.js";
6
+ import type { WaitForEventRequest } from "../pause/wrappers.js";
7
7
  import type { SandboxProvider } from "./sandbox.js";
8
8
 
9
9
  /** The identity of this workflow run. */
@@ -46,8 +46,6 @@ export interface BaseExecutionContext {
46
46
  * PauseExpiredError / PauseSchemaError. See ADR-0006 / ADR-0011.
47
47
  */
48
48
  pause<T = unknown>(req: PauseRequest<T>): Promise<T>;
49
- /** Pause for a typed decision (a `schema` is required). Wrapper over `pause`. */
50
- requestDecision<T>(req: RequestDecisionRequest<T>): Promise<T>;
51
49
  /** Lightweight timed pause — resolves after `durationMs`, no snapshot. */
52
50
  sleep(durationMs: number): Promise<void>;
53
51
  /** Pause until an event resumes by `correlationKey`. Wrapper over `pause`. */
@@ -9,7 +9,7 @@
9
9
  * `workflow-steps/workflow.ts` — it is the cycle-break point.
10
10
  */
11
11
 
12
- import type { SandboxNetworkPolicy } from "../sandbox.js";
12
+ import type { SandboxNetworkPolicy, SandboxSize } from "../sandbox.js";
13
13
  import type { Processor } from "../processors/processor.js";
14
14
 
15
15
  /**
@@ -141,6 +141,16 @@ export interface InvokePolicy {
141
141
  workflows?: string[];
142
142
  }
143
143
 
144
+ /** Sandbox machine resources for a workflow's runs. A coarse size knob
145
+ * today; kept as its own object so finer controls (disk, gpu, …) can be
146
+ * added later without reshaping `WorkflowMetadata`. */
147
+ export interface SandboxResources {
148
+ /** Machine size — `small | medium | large`. Maps to provider specs at
149
+ * create time (Vercel: 2 / 4 / 8 vCPU, 2048 MB RAM per vCPU). Omit →
150
+ * `"small"`. E2B sizing is template-defined and ignores this. */
151
+ size?: SandboxSize;
152
+ }
153
+
144
154
  export interface WorkflowMetadata {
145
155
  /** One-line, human-readable description of what the workflow does.
146
156
  * Surfaced on the dashboard template tile + run page header. Authors
@@ -161,6 +171,8 @@ export interface WorkflowMetadata {
161
171
  outputSchema?: IOSchema;
162
172
  /** All snapshot config — boot source + capture mode. */
163
173
  snapshots?: SnapshotConfig;
174
+ /** Sandbox machine resources (size). Optional; omit → small. */
175
+ resources?: SandboxResources;
164
176
  processors?: readonly Processor[];
165
177
  /** Connector requirements — providers whose APIs this workflow calls.
166
178
  * Dispatch resolves an authorized grant per provider and injects a
@@ -198,6 +210,7 @@ export function extractMetadata(source: Partial<WorkflowMetadata>): WorkflowMeta
198
210
  if (source.inputSchema !== undefined) out.inputSchema = freezeMetadataValue(source.inputSchema);
199
211
  if (source.outputSchema !== undefined) out.outputSchema = freezeMetadataValue(source.outputSchema);
200
212
  if (source.snapshots !== undefined) out.snapshots = Object.freeze({ ...source.snapshots });
213
+ if (source.resources !== undefined) out.resources = Object.freeze({ ...source.resources });
201
214
  if (source.processors !== undefined) out.processors = Object.freeze([...source.processors]);
202
215
  if (source.connectors !== undefined) out.connectors = freezeMetadataValue(source.connectors);
203
216
  if (source.connectorOperation !== undefined) out.connectorOperation = Object.freeze({ ...source.connectorOperation });
@@ -37,8 +37,8 @@ export interface AgentEventSink {
37
37
  emit(event: AgentLifecycleEvent): void | Promise<void>;
38
38
  }
39
39
 
40
- import type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema } from "./workflow-metadata.js";
41
- export type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema };
40
+ import type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema, SandboxResources } from "./workflow-metadata.js";
41
+ export type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema, SandboxResources };
42
42
 
43
43
  /** Turn/iteration budget for `agent(opts)`. Re-exported here so authors
44
44
  * can type per-invoke budget overrides they pass as workflow input. */
@@ -120,6 +120,13 @@ export interface WorkflowDefinition<
120
120
  * `invoke({ snapshots })` overrides this default.
121
121
  */
122
122
  snapshots?: SnapshotConfig;
123
+ /**
124
+ * Sandbox machine size — `small` (default) | `medium` | `large`. Maps to
125
+ * provider machine specs at create (Vercel: 2 / 4 / 8 vCPU, 2048 MB RAM
126
+ * per vCPU). Optional; omit for `small`. E2B sizing is template-defined
127
+ * and ignores this. Per-invocation `invoke({ size })` overrides it.
128
+ */
129
+ resources?: SandboxResources;
123
130
  /**
124
131
  * Outbound network policy for the runner sandbox.
125
132
  * Use "*": [] to allow all traffic while still injecting headers for specific domains.
@@ -239,7 +246,6 @@ function compileRunForm<TOutput, TInput extends Record<string, unknown>>(
239
246
  agentEvents: stepCtx.agentEvents,
240
247
  checkpoint: stepCtx.checkpoint,
241
248
  pause: stepCtx.pause,
242
- requestDecision: stepCtx.requestDecision,
243
249
  sleep: stepCtx.sleep,
244
250
  waitForEvent: stepCtx.waitForEvent,
245
251
  processors: metadata.processors ?? [],
@@ -25,7 +25,7 @@ import { parse as babelParse } from "@babel/parser";
25
25
  import type { File, ExportDefaultDeclaration, CallExpression, Expression, Statement } from "@babel/types";
26
26
  import { importSourceModule } from "./source-loader.js";
27
27
  import type { SnapshotConfig } from "../types/workflow.js";
28
- import type { IOSchema, OutputSchema, ConnectorRequirements, ConnectorOperationTag, InvokePolicy } from "../types/workflow-metadata.js";
28
+ import type { IOSchema, OutputSchema, ConnectorRequirements, ConnectorOperationTag, InvokePolicy, SandboxResources } from "../types/workflow-metadata.js";
29
29
  import type { SandboxNetworkPolicy } from "../sandbox.js";
30
30
  import { isWorkflow } from "../workflow-steps/workflow.js";
31
31
  import type { Workflow } from "../workflow-steps/types.js";
@@ -98,6 +98,8 @@ export interface BundledWorkflow {
98
98
  /** Snapshot config from the workflow definition — `bootFrom` (where to
99
99
  * restore at run start), `save`, `retain`. */
100
100
  snapshots?: SnapshotConfig;
101
+ /** Sandbox machine size declared via `defineWorkflow({ resources: { size } })`. */
102
+ resources?: SandboxResources;
101
103
  workflowPlan: WorkflowPlan;
102
104
  /** Compact JSON-Schema-shaped description of the workflow's input
103
105
  * type. Extracted from the workflow's declared `input` zod schema
@@ -334,6 +336,7 @@ export async function bundleWorkflow(
334
336
  ...(inputSchema !== undefined ? { inputSchema } : {}),
335
337
  ...(outputSchema !== undefined ? { outputSchema } : {}),
336
338
  ...(metadata.snapshots !== undefined ? { snapshots: metadata.snapshots } : {}),
339
+ ...(metadata.resources !== undefined ? { resources: metadata.resources } : {}),
337
340
  ...(metadata.connectors !== undefined ? { connectors: metadata.connectors } : {}),
338
341
  ...(metadata.connectorOperation !== undefined ? { connectorOperation: metadata.connectorOperation } : {}),
339
342
  ...(metadata.invokePolicy !== undefined ? { invokePolicy: metadata.invokePolicy } : {}),
@@ -23,7 +23,7 @@ import type { z } from "zod";
23
23
  import type { Step, Workflow } from "./types.js";
24
24
  import { WORKFLOW_BRAND } from "./types.js";
25
25
  import { extractMetadata } from "../types/workflow-metadata.js";
26
- import type { SnapshotConfig } from "../types/workflow-metadata.js";
26
+ import type { SnapshotConfig, SandboxResources } from "../types/workflow-metadata.js";
27
27
  import type { SandboxNetworkPolicy } from "../sandbox.js";
28
28
  import type { Processor } from "../processors/processor.js";
29
29
 
@@ -51,6 +51,9 @@ export interface StepWorkflowDefinition<TInput, TOutput> {
51
51
  networkPolicy?: SandboxNetworkPolicy;
52
52
  placeholders?: Record<string, string>;
53
53
  snapshots?: SnapshotConfig;
54
+ /** Sandbox machine size — small (default) | medium | large. Vercel maps
55
+ * it to vCPUs; E2B sizing is template-defined. Omit → small. */
56
+ resources?: SandboxResources;
54
57
  processors?: readonly Processor[];
55
58
  }
56
59
 
@@ -24,6 +24,12 @@ export function buildInvokeChild(
24
24
  return (name, input, childOpts) => getChildClient().invokeAndWait(name, input, {
25
25
  ...childOpts,
26
26
  parentRunId: runId,
27
- factorySlug: childOpts?.factorySlug ?? opts.defaultFactorySlug ?? process.env.AGENT_COMPOSE_FACTORY_SLUG ?? "default",
27
+ // The runner sets `AGENT_COMPOSE_FACTORY` (the run's factory slug — see
28
+ // activities.ts loadRunnerEnvs). Default the child to it so it lands in the
29
+ // SAME factory as the parent — the per-run API key is factory-scoped, so a
30
+ // child dispatched into another factory 403s. (Was reading the never-set
31
+ // `AGENT_COMPOSE_FACTORY_SLUG`, silently falling back to "default".)
32
+ factorySlug: childOpts?.factorySlug ?? opts.defaultFactorySlug
33
+ ?? process.env.AGENT_COMPOSE_FACTORY ?? process.env.AGENT_COMPOSE_FACTORY_SLUG ?? "default",
28
34
  });
29
35
  }