@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.
@@ -1,12 +1,16 @@
1
1
  /**
2
- * The three opinionated pause wrappers (ADR-0006 §"SDK surface"), each a thin
2
+ * The two opinionated pause wrappers (ADR-0006 §"SDK surface"), each a thin
3
3
  * closure over `ctx.pause`:
4
4
  *
5
- * - `requestDecision` — pause for a typed human/agent decision (schema required).
6
5
  * - `sleep` — a lightweight timed pause; resolves on its own TTL,
7
6
  * skips the snapshot, returns void.
8
7
  * - `waitForEvent` — pause until an event resumes by correlation key.
9
8
  *
9
+ * A typed human/agent decision is NOT a wrapper — it's a plain `ctx.pause`
10
+ * with a `schema` (and `payload.options` for the dashboard's answer UI). The
11
+ * pause primitive carries the reason (the ask) and the resume value (the
12
+ * resolution); there is no separate `requestDecision`.
13
+ *
10
14
  * Built from a `PauseFn` so the wrapper logic lives in one place and the step
11
15
  * runner just spreads them onto the context next to `pause`.
12
16
  */
@@ -16,16 +20,9 @@ import type { PauseRequest } from "./pause-core.js";
16
20
  /** The public `ctx.pause` callable (always `kind: "custom"`). */
17
21
  export type PauseFn = <T = unknown>(req: PauseRequest<T>) => Promise<T>;
18
22
  /** Internal: pause with an explicit wire `kind`. The wrappers stamp
19
- * `decision`/`sleep`/`event` through this; the public `ctx.pause` is always
23
+ * `sleep`/`event` through this; the public `ctx.pause` is always
20
24
  * `custom` and never exposes it. */
21
25
  export type KindedPauseFn = <T = unknown>(req: PauseRequest<T>, kind: StepPauseRequest["kind"]) => Promise<T>;
22
- export interface RequestDecisionRequest<T> {
23
- reason: string;
24
- payload?: Record<string, unknown>;
25
- /** Required — a decision is always validated against a shape. */
26
- schema: z.ZodType<T>;
27
- ttlMs?: number;
28
- }
29
26
  export interface WaitForEventRequest<T> {
30
27
  reason: string;
31
28
  /** Required — the by-key resume route targets this. */
@@ -34,7 +31,6 @@ export interface WaitForEventRequest<T> {
34
31
  ttlMs?: number;
35
32
  }
36
33
  export interface PauseWrappers {
37
- requestDecision<T>(req: RequestDecisionRequest<T>): Promise<T>;
38
34
  sleep(durationMs: number): Promise<void>;
39
35
  waitForEvent<T = unknown>(req: WaitForEventRequest<T>): Promise<T>;
40
36
  }
@@ -540,6 +540,8 @@ 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
547
  if (source.connectors !== undefined)
@@ -617,7 +619,6 @@ function compileRunForm(def, metadata) {
617
619
  agentEvents: stepCtx.agentEvents,
618
620
  checkpoint: stepCtx.checkpoint,
619
621
  pause: stepCtx.pause,
620
- requestDecision: stepCtx.requestDecision,
621
622
  sleep: stepCtx.sleep,
622
623
  waitForEvent: stepCtx.waitForEvent,
623
624
  processors: metadata.processors ?? []
@@ -812,6 +813,7 @@ class AgentComposeClient {
812
813
  ...opts?.snapshots !== undefined ? { snapshots: opts.snapshots } : {},
813
814
  ...opts?.networkPolicy !== undefined ? { networkPolicy: opts.networkPolicy } : {},
814
815
  ...opts?.placeholders !== undefined ? { placeholders: opts.placeholders } : {},
816
+ ...opts?.size !== undefined ? { size: opts.size } : {},
815
817
  ...parentRunId ? { parentRunId } : {},
816
818
  ...opts?.agentId ? { agentId: opts.agentId } : {}
817
819
  }
@@ -971,6 +973,21 @@ class AgentComposeClient {
971
973
  q2.set("revision", String(opts.revision));
972
974
  return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/content?${q2}`, { responseType: "text" });
973
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
+ }
974
991
  async listFactoryEvents(opts) {
975
992
  const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
976
993
  const q2 = new URLSearchParams;
@@ -1222,6 +1239,7 @@ async function bundleWorkflow(workflowPath, overrides) {
1222
1239
  ...inputSchema !== undefined ? { inputSchema } : {},
1223
1240
  ...outputSchema !== undefined ? { outputSchema } : {},
1224
1241
  ...metadata.snapshots !== undefined ? { snapshots: metadata.snapshots } : {},
1242
+ ...metadata.resources !== undefined ? { resources: metadata.resources } : {},
1225
1243
  ...metadata.connectors !== undefined ? { connectors: metadata.connectors } : {},
1226
1244
  ...metadata.connectorOperation !== undefined ? { connectorOperation: metadata.connectorOperation } : {},
1227
1245
  ...metadata.invokePolicy !== undefined ? { invokePolicy: metadata.invokePolicy } : {}
@@ -1853,7 +1871,7 @@ async function runControlPoller(opts) {
1853
1871
  // src/agent/agent-loop.ts
1854
1872
  var DEFAULT_CLAUDE_MODEL = "claude-fable-5";
1855
1873
  var SAME_BLOCKER_ITERATIONS = 3;
1856
- var STALL_ITERATIONS = 3;
1874
+ var STALL_ITERATIONS = 6;
1857
1875
  var MESSAGE_PREVIEW_CHARS = 400;
1858
1876
  function parseAgentStatus(text) {
1859
1877
  const match = text.match(/<status>([\s\S]*?)<\/status>/);
@@ -2084,7 +2102,15 @@ Re-emit the COMPLETE corrected <response> JSON now: every required field present
2084
2102
  }
2085
2103
  const msg = outputVerdict.value;
2086
2104
  opts.onAgentEvent?.(iteration, msg);
2087
- 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
+ });
2088
2114
  if (msg.type === "init")
2089
2115
  lastSessionId = msg.sessionId;
2090
2116
  if (msg.type === "text")
@@ -2163,9 +2189,9 @@ raw: ${JSON.stringify(rawResponse).slice(0, 400)}
2163
2189
  if (!status && rawResponse === null) {
2164
2190
  if (++iterationsWithoutStatus >= STALL_ITERATIONS)
2165
2191
  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)
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)
2169
2195
  `);
2170
2196
  iteration--;
2171
2197
  continue;
@@ -2746,6 +2772,13 @@ class SandboxUnavailableError extends Error {
2746
2772
  // src/sandbox.ts
2747
2773
  var AGENT_COMPOSE_TAG = process.env.AGENT_COMPOSE_TAG ?? `agent-compose-${process.env.AGENT_COMPOSE_ENV ?? "dev"}`;
2748
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";
2749
2782
  function makeSandboxProvider(sb) {
2750
2783
  const commands = sb.commands;
2751
2784
  return {
@@ -2911,7 +2944,7 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
2911
2944
  let stdout = "";
2912
2945
  let stderr = "";
2913
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));
2914
- try {
2947
+ const collect = async () => {
2915
2948
  await pRetry(async (attempt) => {
2916
2949
  const h = attempt === 1 ? handle : await sb.getCommand(handle.cmdId);
2917
2950
  if (attempt > 1) {
@@ -2930,11 +2963,22 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
2930
2963
  }, { retries: 3, minTimeout: 1000, factor: 2 });
2931
2964
  const finished = await handle.wait();
2932
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());
2933
2973
  } catch (err) {
2934
- if (signal?.aborted) {
2974
+ if (err instanceof Error && err.message.startsWith("command timed out after"))
2975
+ throw err;
2976
+ if (signal?.aborted)
2935
2977
  throw new Error(`command timed out after ${opts?.timeoutMs}ms: ${cmd.slice(0, 200)}`);
2936
- }
2937
2978
  return asUnavailable(err, false);
2979
+ } finally {
2980
+ if (timer)
2981
+ clearTimeout(timer);
2938
2982
  }
2939
2983
  }
2940
2984
  },
@@ -3011,7 +3055,8 @@ var SANDBOX_PROVIDERS = {
3011
3055
  const np = opts.networkPolicy ? { networkPolicy: toVercelNetworkPolicy(opts.networkPolicy) } : {};
3012
3056
  const tags = buildVercelTags(opts.metadata);
3013
3057
  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 });
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 });
3015
3060
  return makeVercelSandboxProvider(sb, opts.envs);
3016
3061
  },
3017
3062
  reconnect: async (sandboxId) => {
@@ -3057,7 +3102,7 @@ var SANDBOX_PROVIDERS = {
3057
3102
  },
3058
3103
  e2b: {
3059
3104
  requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
3060
- create: async ({ template, timeoutMs, networkPolicy: _np, ...rest }) => {
3105
+ create: async ({ template, timeoutMs, networkPolicy: _np, size: _size, ...rest }) => {
3061
3106
  const maxMs = e2bMaxSandboxMs();
3062
3107
  if (timeoutMs > maxMs) {
3063
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.`);
@@ -3099,7 +3144,7 @@ var SANDBOX_PROVIDERS = {
3099
3144
  },
3100
3145
  "e2b-desktop": {
3101
3146
  requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
3102
- create: async ({ template, timeoutMs, networkPolicy: _np, ...rest }) => {
3147
+ create: async ({ template, timeoutMs, networkPolicy: _np, size: _size, ...rest }) => {
3103
3148
  if (!template)
3104
3149
  throw new Error("E2B Desktop provider requires an explicit `template` (Dockerfile-based — no default base image)");
3105
3150
  return makeDesktopSandboxProvider(await Desktop.create(template, { ...rest, timeoutMs: Math.min(timeoutMs, e2bMaxSandboxMs()) }));
@@ -3283,14 +3328,6 @@ function scopedCheckpoint(scope) {
3283
3328
  // src/pause/wrappers.ts
3284
3329
  function buildPauseWrappers(pause) {
3285
3330
  return {
3286
- requestDecision(req) {
3287
- return pause({
3288
- reason: req.reason,
3289
- schema: req.schema,
3290
- ...req.payload !== undefined ? { payload: req.payload } : {},
3291
- ...req.ttlMs !== undefined ? { ttlMs: req.ttlMs } : {}
3292
- }, "decision");
3293
- },
3294
3331
  sleep(durationMs) {
3295
3332
  return pause({
3296
3333
  reason: `sleep ${durationMs}ms`,
@@ -3600,7 +3637,7 @@ function buildInvokeChild(runId, opts = {}) {
3600
3637
  return (name, input, childOpts) => getChildClient().invokeAndWait(name, input, {
3601
3638
  ...childOpts,
3602
3639
  parentRunId: runId,
3603
- 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"
3604
3641
  });
3605
3642
  }
3606
3643
  // src/workflow-steps/step.ts
@@ -3967,6 +4004,149 @@ async function serveStep(handler) {
3967
4004
  import { z as z11 } from "zod";
3968
4005
  import { randomUUID as randomUUID2 } from "node:crypto";
3969
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
+
3970
4150
  // src/agent/protocol-suffix.md
3971
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';
3972
4152
 
@@ -4082,7 +4262,28 @@ function resolveAgentId(explicitId) {
4082
4262
  return `step${activeCall.stepIndex}-agent-${activeCall.callIndex}`;
4083
4263
  }
4084
4264
  async function agent(opts) {
4085
- 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
+ }
4086
4287
  const agentId = resolveAgentId(opts.agentId);
4087
4288
  const callbackUrl = process.env.AGENT_COMPOSE_URL;
4088
4289
  const callbackToken = process.env.AGENT_COMPOSE_RUN_TOKEN;
package/dist/sandbox.d.ts CHANGED
@@ -39,6 +39,27 @@ export type SandboxNetworkPolicy = "allow-all" | "deny-all" | {
39
39
  allow?: string[] | Record<string, SandboxNetworkAllowRule[]>;
40
40
  subnets?: SandboxNetworkSubnetPolicy;
41
41
  };
42
+ /** Sandbox machine size. A coarse small/medium/large knob that maps to
43
+ * provider machine specs at create time. Vercel honours it natively via
44
+ * `resources.vcpus` (2048 MB RAM per vCPU). E2B sizing is baked into the
45
+ * template, so E2B ignores this field. Default: "small". */
46
+ /** Sandbox hardware SKU. Named for the actual machine spec (vCPU + RAM) rather
47
+ * than abstract t-shirt sizes. Memory is always 2048 MB per vCPU:
48
+ * 2vcpu-4gb = 2 vCPU / 4 GiB (Vercel's own default machine)
49
+ * 4vcpu-8gb = 4 vCPU / 8 GiB
50
+ * 8vcpu-16gb = 8 vCPU / 16 GiB (per-sandbox ceiling on STANDARD accounts —
51
+ * probed live: 16 & 32 vCPU 400 on dev)
52
+ * 32vcpu-64gb = 32 vCPU / 64 GiB (ENTERPRISE ONLY — standard accounts reject >8 vCPU) */
53
+ export type SandboxSize = "2vcpu-4gb" | "4vcpu-8gb" | "8vcpu-16gb" | "32vcpu-64gb";
54
+ /** SKU → Vercel vCPU count (RAM follows at 2048 MB/vCPU). */
55
+ export declare const SANDBOX_VCPUS: Record<SandboxSize, number>;
56
+ /** SDK fallback size when neither the caller nor the deployment specifies one.
57
+ * Deliberately conservative — the OPERATIONAL default is chosen per-environment
58
+ * by the server via the `SANDBOX_DEFAULT_SIZE` env var (prod = Enterprise →
59
+ * `32vcpu-64gb`; dev = `8vcpu-16gb`, since the dev account caps at 8 vCPU).
60
+ * TODO(sandbox-size): the prod default is temporarily `32vcpu-64gb` for a
61
+ * memory-hungry workload — lower it back when no longer needed. */
62
+ export declare const DEFAULT_SANDBOX_SIZE: SandboxSize;
42
63
  export interface SandboxCreateOpts {
43
64
  envs: Record<string, string>;
44
65
  metadata: Record<string, string>;
@@ -46,6 +67,9 @@ export interface SandboxCreateOpts {
46
67
  /** Provider-specific template/snapshot identifier. E2B: template id or
47
68
  * snapshot id (omit → E2B's default base). Vercel: snapshot id (omit → node24). */
48
69
  template?: string;
70
+ /** Machine size. Vercel maps it to `resources.vcpus`; E2B ignores it
71
+ * (size is template-defined). Omit → `DEFAULT_SANDBOX_SIZE`. */
72
+ size?: SandboxSize;
49
73
  /** Outbound request policy with header transforms — ONE shape for every
50
74
  * provider; only the enforcement point differs. Vercel's firewall
51
75
  * enforces + injects natively from this value. E2B enforces it via an
@@ -48,7 +48,7 @@ export type StepResult<TOutput = unknown> = {
48
48
  * it so the runtime type and the parsed shape can't drift.
49
49
  *
50
50
  * Loose by design (the inner zod shape stops at orchestration fields
51
- * the engine needs): the SDK wrapper layer (`requestDecision` /
51
+ * the engine needs): the SDK wrapper layer (`sleep` /
52
52
  * `sleep` / `waitForEvent`) owns its own payload contract, and the
53
53
  * engine treats `payload` as opaque. */
54
54
  export declare const StepPauseRequestSchema: z.ZodObject<{
@@ -2,7 +2,7 @@
2
2
  import type { InvokeAndWaitOptions, RunStatus } from "../client.js";
3
3
  import type { RequestContext } from "../request-context/request-context.js";
4
4
  import type { PauseRequest } from "../pause/pause-core.js";
5
- import type { RequestDecisionRequest, WaitForEventRequest } from "../pause/wrappers.js";
5
+ import type { WaitForEventRequest } from "../pause/wrappers.js";
6
6
  import type { SandboxProvider } from "./sandbox.js";
7
7
  /** The identity of this workflow run. */
8
8
  export interface WorkflowRun {
@@ -38,8 +38,6 @@ export interface BaseExecutionContext {
38
38
  * PauseExpiredError / PauseSchemaError. See ADR-0006 / ADR-0011.
39
39
  */
40
40
  pause<T = unknown>(req: PauseRequest<T>): Promise<T>;
41
- /** Pause for a typed decision (a `schema` is required). Wrapper over `pause`. */
42
- requestDecision<T>(req: RequestDecisionRequest<T>): Promise<T>;
43
41
  /** Lightweight timed pause — resolves after `durationMs`, no snapshot. */
44
42
  sleep(durationMs: number): Promise<void>;
45
43
  /** Pause until an event resumes by `correlationKey`. Wrapper over `pause`. */
@@ -8,7 +8,7 @@
8
8
  * Keep this module free of value imports from `types/workflow.ts` or
9
9
  * `workflow-steps/workflow.ts` — it is the cycle-break point.
10
10
  */
11
- import type { SandboxNetworkPolicy } from "../sandbox.js";
11
+ import type { SandboxNetworkPolicy, SandboxSize } from "../sandbox.js";
12
12
  import type { Processor } from "../processors/processor.js";
13
13
  /**
14
14
  * Workflow-level metadata read by the server at registration. Lives on
@@ -134,6 +134,15 @@ export interface InvokePolicy {
134
134
  apiKeys?: string[];
135
135
  workflows?: string[];
136
136
  }
137
+ /** Sandbox machine resources for a workflow's runs. A coarse size knob
138
+ * today; kept as its own object so finer controls (disk, gpu, …) can be
139
+ * added later without reshaping `WorkflowMetadata`. */
140
+ export interface SandboxResources {
141
+ /** Machine size — `small | medium | large`. Maps to provider specs at
142
+ * create time (Vercel: 2 / 4 / 8 vCPU, 2048 MB RAM per vCPU). Omit →
143
+ * `"small"`. E2B sizing is template-defined and ignores this. */
144
+ size?: SandboxSize;
145
+ }
137
146
  export interface WorkflowMetadata {
138
147
  /** One-line, human-readable description of what the workflow does.
139
148
  * Surfaced on the dashboard template tile + run page header. Authors
@@ -154,6 +163,8 @@ export interface WorkflowMetadata {
154
163
  outputSchema?: IOSchema;
155
164
  /** All snapshot config — boot source + capture mode. */
156
165
  snapshots?: SnapshotConfig;
166
+ /** Sandbox machine resources (size). Optional; omit → small. */
167
+ resources?: SandboxResources;
157
168
  processors?: readonly Processor[];
158
169
  /** Connector requirements — providers whose APIs this workflow calls.
159
170
  * Dispatch resolves an authorized grant per provider and injects a
@@ -27,8 +27,8 @@ export type { WorkflowMetadata } from "./workflow-metadata.js";
27
27
  export interface AgentEventSink {
28
28
  emit(event: AgentLifecycleEvent): void | Promise<void>;
29
29
  }
30
- import type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema } from "./workflow-metadata.js";
31
- export type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema };
30
+ import type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema, SandboxResources } from "./workflow-metadata.js";
31
+ export type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema, SandboxResources };
32
32
  /** Turn/iteration budget for `agent(opts)`. Re-exported here so authors
33
33
  * can type per-invoke budget overrides they pass as workflow input. */
34
34
  export interface AgentBudget {
@@ -100,6 +100,13 @@ export interface WorkflowDefinition<TOutput = unknown, TInput extends Record<str
100
100
  * `invoke({ snapshots })` overrides this default.
101
101
  */
102
102
  snapshots?: SnapshotConfig;
103
+ /**
104
+ * Sandbox machine size — `small` (default) | `medium` | `large`. Maps to
105
+ * provider machine specs at create (Vercel: 2 / 4 / 8 vCPU, 2048 MB RAM
106
+ * per vCPU). Optional; omit for `small`. E2B sizing is template-defined
107
+ * and ignores this. Per-invocation `invoke({ size })` overrides it.
108
+ */
109
+ resources?: SandboxResources;
103
110
  /**
104
111
  * Outbound network policy for the runner sandbox.
105
112
  * Use "*": [] to allow all traffic while still injecting headers for specific domains.
@@ -20,7 +20,7 @@
20
20
  * manifest's `sourceHash` against the source it received.
21
21
  */
22
22
  import type { SnapshotConfig } from "../types/workflow.js";
23
- import type { IOSchema, ConnectorRequirements, ConnectorOperationTag, InvokePolicy } from "../types/workflow-metadata.js";
23
+ import type { IOSchema, ConnectorRequirements, ConnectorOperationTag, InvokePolicy, SandboxResources } from "../types/workflow-metadata.js";
24
24
  import type { SandboxNetworkPolicy } from "../sandbox.js";
25
25
  import { type WorkflowPlan } from "../types/workflow-plan.js";
26
26
  /** Bumped when the manifest contract changes in a way the server should
@@ -84,6 +84,8 @@ export interface BundledWorkflow {
84
84
  /** Snapshot config from the workflow definition — `bootFrom` (where to
85
85
  * restore at run start), `save`, `retain`. */
86
86
  snapshots?: SnapshotConfig;
87
+ /** Sandbox machine size declared via `defineWorkflow({ resources: { size } })`. */
88
+ resources?: SandboxResources;
87
89
  workflowPlan: WorkflowPlan;
88
90
  /** Compact JSON-Schema-shaped description of the workflow's input
89
91
  * type. Extracted from the workflow's declared `input` zod schema
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import type { z } from "zod";
22
22
  import type { Step, Workflow } from "./types.js";
23
- import type { SnapshotConfig } from "../types/workflow-metadata.js";
23
+ import type { SnapshotConfig, SandboxResources } from "../types/workflow-metadata.js";
24
24
  import type { SandboxNetworkPolicy } from "../sandbox.js";
25
25
  import type { Processor } from "../processors/processor.js";
26
26
  export interface WorkflowBuilder<TInput, TCurrent> {
@@ -46,6 +46,9 @@ export interface StepWorkflowDefinition<TInput, TOutput> {
46
46
  networkPolicy?: SandboxNetworkPolicy;
47
47
  placeholders?: Record<string, string>;
48
48
  snapshots?: SnapshotConfig;
49
+ /** Sandbox machine size — small (default) | medium | large. Vercel maps
50
+ * it to vCPUs; E2B sizing is template-defined. Omit → small. */
51
+ resources?: SandboxResources;
49
52
  processors?: readonly Processor[];
50
53
  }
51
54
  export declare function createStepWorkflow<TInput, TOutput>(opts: StepWorkflowDefinition<TInput, TOutput>): WorkflowBuilder<TInput, TInput>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-compose/sdk",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "Client library for agent-compose — define agents, runtimes, and workflows, and invoke them against an agent-compose server.",
5
5
  "license": "MIT",
6
6
  "repository": {