@agent-compose/sdk 0.5.8 → 0.6.0

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 (78) hide show
  1. package/dist/agent/agent-context.d.ts +1 -1
  2. package/dist/agent/agent-loop.d.ts +14 -12
  3. package/dist/agent/pause-client.d.ts +50 -0
  4. package/dist/agent/pause-client.test.d.ts +1 -0
  5. package/dist/agent/steer-control.d.ts +22 -6
  6. package/dist/client.d.ts +12 -1
  7. package/dist/index.d.ts +7 -5
  8. package/dist/index.js +2379 -1463
  9. package/dist/pause/checkpoint.d.ts +27 -10
  10. package/dist/pause/manager.d.ts +1 -0
  11. package/dist/pause/pause-core.d.ts +23 -0
  12. package/dist/pause/state-dir.d.ts +1 -1
  13. package/dist/processors/builtins.d.ts +20 -1
  14. package/dist/processors/gate-pause.d.ts +46 -0
  15. package/dist/processors/gate-pause.test.d.ts +1 -0
  16. package/dist/processors/index.d.ts +3 -1
  17. package/dist/processors/processor.d.ts +13 -0
  18. package/dist/runtimes/_acp-client.d.ts +140 -0
  19. package/dist/runtimes/_cli-agent.d.ts +155 -3
  20. package/dist/runtimes/amp.d.ts +2 -2
  21. package/dist/runtimes/cli-agent-acp-live.test.d.ts +30 -0
  22. package/dist/runtimes/cli-agent.test.d.ts +22 -6
  23. package/dist/runtimes/codex.d.ts +7 -2
  24. package/dist/runtimes/openai-desktop.js +2365 -1463
  25. package/dist/runtimes/vercel.js +389 -2
  26. package/dist/sandbox.d.ts +113 -19
  27. package/dist/step-invocation/__tests__/background-invoker.test.d.ts +1 -0
  28. package/dist/step-invocation/index.d.ts +2 -1
  29. package/dist/step-invocation/invoker.d.ts +36 -0
  30. package/dist/types/__tests__/environment-build-flag.test.d.ts +1 -0
  31. package/dist/types/__tests__/workflow-metadata-provider.test.d.ts +1 -0
  32. package/dist/types/execution-context.d.ts +0 -8
  33. package/dist/types/protocol.d.ts +32 -1
  34. package/dist/types/runtime.d.ts +14 -0
  35. package/dist/types/sandbox-environment.d.ts +6 -1
  36. package/dist/types/sandbox.d.ts +86 -6
  37. package/dist/types/workflow-metadata.d.ts +40 -10
  38. package/dist/types/workflow.d.ts +22 -6
  39. package/dist/utils/bundler.d.ts +5 -1
  40. package/dist/workflow-steps/observability.d.ts +28 -2
  41. package/dist/workflow-steps/types.d.ts +11 -7
  42. package/dist/workflow-steps/workflow.d.ts +3 -2
  43. package/package.json +3 -2
  44. package/src/agent/agent-context.ts +14 -6
  45. package/src/agent/agent-loop.ts +32 -10
  46. package/src/agent/pause-client.ts +108 -0
  47. package/src/agent/run-agent.ts +9 -4
  48. package/src/agent/steer-control.ts +21 -7
  49. package/src/client.ts +35 -1
  50. package/src/index.ts +20 -2
  51. package/src/pause/checkpoint.ts +33 -14
  52. package/src/pause/manager.ts +2 -2
  53. package/src/pause/pause-core.ts +35 -0
  54. package/src/pause/state-dir.ts +2 -2
  55. package/src/processors/builtins.ts +44 -1
  56. package/src/processors/gate-pause.ts +94 -0
  57. package/src/processors/index.ts +7 -0
  58. package/src/processors/processor.ts +13 -0
  59. package/src/runtimes/_acp-client.ts +516 -0
  60. package/src/runtimes/_cli-agent.ts +416 -3
  61. package/src/runtimes/claude.ts +31 -3
  62. package/src/runtimes/codex.ts +21 -1
  63. package/src/runtimes/vercel.ts +4 -1
  64. package/src/sandbox.ts +426 -56
  65. package/src/step-invocation/index.ts +2 -1
  66. package/src/step-invocation/invoker.ts +195 -84
  67. package/src/types/execution-context.ts +0 -8
  68. package/src/types/protocol.ts +27 -1
  69. package/src/types/runtime.ts +14 -0
  70. package/src/types/sandbox-environment.ts +12 -1
  71. package/src/types/sandbox.ts +84 -6
  72. package/src/types/workflow-metadata.ts +42 -10
  73. package/src/types/workflow.ts +22 -7
  74. package/src/utils/bundler.ts +6 -1
  75. package/src/workflow-steps/observability.ts +51 -5
  76. package/src/workflow-steps/runner.ts +9 -5
  77. package/src/workflow-steps/types.ts +11 -7
  78. package/src/workflow-steps/workflow.ts +3 -2
@@ -9,9 +9,15 @@
9
9
  * `workflow-steps/workflow.ts` — it is the cycle-break point.
10
10
  */
11
11
 
12
- import type { SandboxNetworkPolicy, SandboxSize } from "../sandbox.js";
12
+ import type { SandboxNetworkPolicy, SandboxSize, SandboxProviderName } from "../sandbox.js";
13
13
  import type { Processor } from "../processors/processor.js";
14
14
 
15
+ /** The sandbox providers selectable per-workflow. A narrowing of
16
+ * `SandboxProviderName` to the two substrates that execute step workloads —
17
+ * `e2b-desktop` (registry-only, never a workflow's runtime) is deliberately
18
+ * excluded so a workflow can only ask for a substrate that actually runs steps. */
19
+ export type WorkflowSandboxProvider = Extract<SandboxProviderName, "vercel" | "e2b">;
20
+
15
21
  /**
16
22
  * Workflow-level metadata read by the server at registration. Lives on
17
23
  * every `Workflow` as `workflow.metadata`, regardless of which form of
@@ -77,11 +83,14 @@ export interface IOSchema {
77
83
  * output-only release. New code should prefer `IOSchema`. */
78
84
  export type OutputSchema = IOSchema;
79
85
 
80
- /** Per-connector HTTP request matcher (Tier-2 capability narrowing). The
81
- * iron-proxy / firewall consults these when deciding whether to attach the
82
- * brokered Authorization header to an outbound request. A request to the
83
- * connector host whose method is not in `methods`, or whose path matches no
84
- * entry in `pathPrefixes`, is refused (403) and the token is WITHHELD. */
86
+ /** Per-connector HTTP request matcher (Tier-2 capability narrowing). Vercel's
87
+ * firewall consults these when deciding whether to attach the brokered
88
+ * Authorization header to an outbound request: a request to the connector
89
+ * host whose method is not in `methods`, or whose path matches no entry in
90
+ * `pathPrefixes`, goes out WITHOUT the token. NOT enforced on E2B — its
91
+ * native rules carry only a header transform, no method/path matcher, so the
92
+ * token rides every request to an allowed connector host there (the host
93
+ * allowlist still confines which hosts are reachable). See `toE2bNetwork`. */
85
94
  export interface ConnectorRequestRules {
86
95
  /** Allowed HTTP methods (upper-case). Omit = any method (subject to
87
96
  * `access`). */
@@ -145,10 +154,19 @@ export interface InvokePolicy {
145
154
  * today; kept as its own object so finer controls (disk, gpu, …) can be
146
155
  * added later without reshaping `WorkflowMetadata`. */
147
156
  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. */
157
+ /** Machine hardware SKU — one of the `SandboxSize` vCPU strings
158
+ * (`2vcpu-4gb` | `4vcpu-8gb` | `8vcpu-16gb` | `32vcpu-64gb`). Maps to
159
+ * provider specs at create time (Vercel: 2 / 4 / 8 / 32 vCPU, 2048 MB RAM
160
+ * per vCPU). Omit → the smallest SKU. E2B sizing is template-defined and
161
+ * ignores this. */
151
162
  size?: SandboxSize;
163
+ /** Sandbox provider this workflow's runs execute on — `"vercel"` or
164
+ * `"e2b"`. Optional and additive: omit and the run resolves to the
165
+ * platform default (`vercel`). Set explicitly to pin a workflow to a
166
+ * substrate regardless of the deployment default. Snapshot formats are
167
+ * provider-specific, so the resolved provider is stamped on the run row
168
+ * at first dispatch and reused across pause/resume/replay. */
169
+ provider?: WorkflowSandboxProvider;
152
170
  }
153
171
 
154
172
  export interface WorkflowMetadata {
@@ -171,7 +189,8 @@ export interface WorkflowMetadata {
171
189
  outputSchema?: IOSchema;
172
190
  /** All snapshot config — boot source + capture mode. */
173
191
  snapshots?: SnapshotConfig;
174
- /** Sandbox machine resources (size). Optional; omit → small. */
192
+ /** Sandbox machine resources — size + provider. Optional; omit → smallest
193
+ * SKU on the platform-default provider (`vercel`). */
175
194
  resources?: SandboxResources;
176
195
  processors?: readonly Processor[];
177
196
  /** Connector requirements — providers whose APIs this workflow calls.
@@ -184,6 +203,18 @@ export interface WorkflowMetadata {
184
203
  /** Tier-1 invoke ACL — who may dispatch this connector-brokering workflow.
185
204
  * See `InvokePolicy`. */
186
205
  invokePolicy?: InvokePolicy;
206
+ /** Set by `defineSandboxEnvironment` to mark this workflow as an ENVIRONMENT
207
+ * BUILD — a setup-only workflow whose job is to leave its VM configured and
208
+ * snapshot it (base-env / agent-env). Environment builds build a platform
209
+ * IMAGE and never use the shared factory drive, so the server SKIPS mounting
210
+ * /factory for them: a live Archil mount baked into the captured snapshot
211
+ * fails the NEXT boot's re-mount ("an older Archil process is still running
212
+ * for this mountpoint"), degrading /factory for every workflow booting from
213
+ * that snapshot. Absent on ordinary workflows — which mount /factory exactly
214
+ * as before. Optional + additive: an ABSENT flag contributes nothing to the
215
+ * canonical metadata hash (frozen-metadata rule), so existing workflows are
216
+ * not forced to re-register. */
217
+ environmentBuild?: boolean;
187
218
  }
188
219
 
189
220
  /**
@@ -215,6 +246,7 @@ export function extractMetadata(source: Partial<WorkflowMetadata>): WorkflowMeta
215
246
  if (source.connectors !== undefined) out.connectors = freezeMetadataValue(source.connectors);
216
247
  if (source.connectorOperation !== undefined) out.connectorOperation = Object.freeze({ ...source.connectorOperation });
217
248
  if (source.invokePolicy !== undefined) out.invokePolicy = freezeMetadataValue(source.invokePolicy);
249
+ if (source.environmentBuild !== undefined) out.environmentBuild = source.environmentBuild;
218
250
  return Object.freeze(out);
219
251
  }
220
252
 
@@ -53,9 +53,16 @@ export interface WorkflowCtx<TInput extends Record<string, unknown> = Record<str
53
53
  /** Persist key-value metadata on the run record (e.g. prUrl, planUrl). */
54
54
  setMetadata: (data: Record<string, unknown>) => Promise<void>;
55
55
  /**
56
- * Wrap a named step for observability. Emits step_started / step_completed /
57
- * step_failed lifecycle events with duration. Use for long phases you want
56
+ * Durable named step (ADR-0012). Runs `fn` once and memoises its result to
57
+ * the sandbox state-dir; on a pause-resume re-entry the recorded value is
58
+ * returned and `fn` is NOT re-run (a duration-0 "restored" sub-step). Also
59
+ * emits substep_started / substep_completed / substep_failed lifecycle
60
+ * events with duration — use for long phases you want both durable and
58
61
  * visible on the run's timeline (setup, external API calls, submit).
62
+ *
63
+ * Names must be unique within a step body (they key the memoise file).
64
+ * A body that pauses is never memoised — the resume re-runs it. For
65
+ * cross-process side effects (DB writes, emails) use `invokeChild`.
59
66
  */
60
67
  step<T>(name: string, fn: () => Promise<T>): Promise<T>;
61
68
  /** Pass to `agent({ events: ctx.agentEvents })` to stream agent lifecycle events. */
@@ -121,10 +128,12 @@ export interface WorkflowDefinition<
121
128
  */
122
129
  snapshots?: SnapshotConfig;
123
130
  /**
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.
131
+ * Sandbox resources — machine SKU (`size`, a `SandboxSize` vCPU string:
132
+ * `2vcpu-4gb` | `4vcpu-8gb` | `8vcpu-16gb` | `32vcpu-64gb`) and `provider`
133
+ * (`vercel` | `e2b`). `size` maps to provider machine specs at create
134
+ * (Vercel: vCPUs, 2048 MB RAM per vCPU); E2B sizing is template-defined and
135
+ * ignores it. Optional; omit → smallest SKU on the default provider.
136
+ * Per-invocation `invoke({ size })` overrides the size.
128
137
  */
129
138
  resources?: SandboxResources;
130
139
  /**
@@ -198,6 +207,13 @@ export interface WorkflowDefinition<
198
207
  * invokePolicy: { users: "owner", workflows: ["nightly-orchestrator"] }
199
208
  */
200
209
  invokePolicy?: InvokePolicy;
210
+ /**
211
+ * Internal — set by `defineSandboxEnvironment`, not by workflow authors.
212
+ * Marks the workflow as an environment build (base-env / agent-env) so the
213
+ * server skips mounting the shared factory drive for its runs (#13). See
214
+ * `WorkflowMetadata.environmentBuild`.
215
+ */
216
+ environmentBuild?: boolean;
201
217
  }
202
218
 
203
219
  /**
@@ -244,7 +260,6 @@ function compileRunForm<TOutput, TInput extends Record<string, unknown>>(
244
260
  setMetadata: stepCtx.setMetadata,
245
261
  step: stepCtx.step,
246
262
  agentEvents: stepCtx.agentEvents,
247
- checkpoint: stepCtx.checkpoint,
248
263
  pause: stepCtx.pause,
249
264
  sleep: stepCtx.sleep,
250
265
  waitForEvent: stepCtx.waitForEvent,
@@ -98,7 +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 } })`. */
101
+ /** Sandbox resources declared via `defineWorkflow({ resources })` — machine
102
+ * SKU (`size`) and `provider` (`vercel` | `e2b`). */
102
103
  resources?: SandboxResources;
103
104
  workflowPlan: WorkflowPlan;
104
105
  /** Compact JSON-Schema-shaped description of the workflow's input
@@ -114,6 +115,9 @@ export interface BundledWorkflow {
114
115
  /** Tier-1 invoke ACL — who may dispatch this connector-brokering
115
116
  * workflow (`defineWorkflow({ invokePolicy })`). */
116
117
  invokePolicy?: InvokePolicy;
118
+ /** Set by `defineSandboxEnvironment` — marks an environment build so the
119
+ * server skips the /factory mount for its runs (#13). */
120
+ environmentBuild?: boolean;
117
121
  }
118
122
 
119
123
  async function bundle(path: string, label: string): Promise<string> {
@@ -340,6 +344,7 @@ export async function bundleWorkflow(
340
344
  ...(metadata.connectors !== undefined ? { connectors: metadata.connectors } : {}),
341
345
  ...(metadata.connectorOperation !== undefined ? { connectorOperation: metadata.connectorOperation } : {}),
342
346
  ...(metadata.invokePolicy !== undefined ? { invokePolicy: metadata.invokePolicy } : {}),
347
+ ...(metadata.environmentBuild !== undefined ? { environmentBuild: metadata.environmentBuild } : {}),
343
348
  };
344
349
  }
345
350
 
@@ -32,15 +32,21 @@
32
32
  import type { AgentLifecycleEvent } from "../agent/agent-loop.js";
33
33
  import type { AgentEventSink } from "../types/workflow.js";
34
34
  import type { LiveAgentEventEmitter } from "./run-callback.js";
35
- import { PauseSignal, isPauseSignal } from "../pause/pause-core.js";
35
+ import { isPauseSignal } from "../pause/pause-core.js";
36
+ import type { ScopedMemoize } from "../pause/checkpoint.js";
36
37
 
37
38
  /** One named sub-step (from `ctx.step("name", async () => ...)`).
38
- * Becomes a `workflow_substep_*` lifecycle event on the run timeline. */
39
+ * Becomes a `workflow_substep_*` lifecycle event on the run timeline.
40
+ *
41
+ * `restored` is the durable-step resume case (ADR-0012): the body did NOT
42
+ * re-run — its memoised result was read from the state-dir checkpoint a
43
+ * prior subprocess wrote. Always `durationMs: 0` (no work happened this
44
+ * pass) and never carries `error`. */
39
45
  export interface SubStepEvent {
40
46
  name: string;
41
47
  startedAt: number;
42
48
  durationMs: number;
43
- status: "completed" | "failed";
49
+ status: "completed" | "failed" | "restored";
44
50
  /** Present when status="failed" — the user error's message. */
45
51
  error?: string;
46
52
  }
@@ -76,22 +82,60 @@ export class StepObservabilityCollector {
76
82
  private events: AgentLifecycleEvent[] = [];
77
83
  private subSteps: SubStepEvent[] = [];
78
84
  private readonly liveEmitter?: LiveAgentEventEmitter;
85
+ /** State-dir memoise bound to this step's `step<idx>` scope. Present in
86
+ * the sandbox runner (durable `ctx.step`); absent in-process so unit
87
+ * tests record observability without touching disk. */
88
+ private readonly memoize?: ScopedMemoize;
89
+ /** Sub-step names already used in THIS step body. A durable `ctx.step`
90
+ * memoises by name, so a reused name would alias two distinct bodies
91
+ * onto one checkpoint file — guarded by throwing on reuse (ADR-0012). */
92
+ private readonly usedStepNames: Set<string> = new Set();
79
93
  /** Seqs of events the server confirmed via the live route. */
80
94
  private readonly ackedSeqs: Set<number> = new Set();
81
95
  /** Promises for in-flight live emits — awaited at snapshot time. */
82
96
  private readonly inFlight: Set<Promise<void>> = new Set();
83
97
 
84
- constructor(opts: { liveEmitter?: LiveAgentEventEmitter } = {}) {
98
+ constructor(opts: { liveEmitter?: LiveAgentEventEmitter; memoize?: ScopedMemoize } = {}) {
85
99
  this.liveEmitter = opts.liveEmitter;
100
+ this.memoize = opts.memoize;
86
101
  }
87
102
 
88
103
  readonly setMetadata = async (data: Record<string, unknown>): Promise<void> => {
89
104
  Object.assign(this.metadata, data);
90
105
  };
91
106
 
107
+ /**
108
+ * Durable named sub-step (ADR-0012). The body's result is memoised to the
109
+ * state-dir checkpoint keyed `step<idx>.<name>`; on a pause-resume re-entry
110
+ * the value is read from disk and `fn` is NOT re-run (recorded as a
111
+ * duration-0 `"restored"` sub-step). A body that PAUSES (PauseSignal)
112
+ * propagates BEFORE any memoise write — partial work is never stored, so
113
+ * the resume re-runs it.
114
+ *
115
+ * When no memoise store is wired (in-process tests), the body always runs
116
+ * and is recorded as a normal timed `"completed"` / `"failed"` sub-step.
117
+ */
92
118
  readonly step = async <T>(name: string, fn: () => Promise<T>): Promise<T> => {
119
+ if (this.usedStepNames.has(name)) {
120
+ throw new Error(
121
+ `ctx.step("${name}", …) reuses a step name already used in this step body. ` +
122
+ `Durable step names must be unique — they key the resume-time memoise file.`,
123
+ );
124
+ }
125
+ this.usedStepNames.add(name);
126
+
93
127
  const startedAt = Date.now();
94
128
  try {
129
+ if (this.memoize) {
130
+ const { value, restored } = await this.memoize(name, fn);
131
+ this.subSteps.push({
132
+ name,
133
+ startedAt,
134
+ durationMs: restored ? 0 : Date.now() - startedAt,
135
+ status: restored ? "restored" : "completed",
136
+ });
137
+ return value;
138
+ }
95
139
  const result = await fn();
96
140
  this.subSteps.push({
97
141
  name,
@@ -102,7 +146,9 @@ export class StepObservabilityCollector {
102
146
  return result;
103
147
  } catch (err) {
104
148
  // A pause inside ctx.step is control flow, not a failed sub-step — let
105
- // it propagate untouched so serveStep emits the pause sentinel.
149
+ // it propagate untouched so serveStep emits the pause sentinel. The
150
+ // memoise write is skipped (it only runs after `fn` resolves), so no
151
+ // partial result is stored — the resume re-enters and re-runs the body.
106
152
  if (isPauseSignal(err)) throw err;
107
153
  this.subSteps.push({
108
154
  name,
@@ -29,7 +29,7 @@ import type { SandboxProvider } from "../types/sandbox.js";
29
29
  import type { WorkflowRun, WorkflowCtx } from "../types/workflow.js";
30
30
  import { StepObservabilityCollector, type StepObservability } from "./observability.js";
31
31
  import type { LiveAgentEventEmitter } from "./run-callback.js";
32
- import { scopedCheckpoint } from "../pause/checkpoint.js";
32
+ import { scopedMemoize } from "../pause/checkpoint.js";
33
33
  import { corePause, PauseSignal, isPauseSignal, type PauseRequest } from "../pause/pause-core.js";
34
34
  import type { StepPauseRequest } from "../step-invocation/types.js";
35
35
  import { buildPauseWrappers, type PauseFn, type KindedPauseFn } from "../pause/wrappers.js";
@@ -129,7 +129,10 @@ export async function runWorkflowSingleStep(opts: RunWorkflowSingleStepOpts): Pr
129
129
  if (!step) throw new Error(`Step index ${opts.stepIndex} not found in workflow "${opts.workflow.id}"`);
130
130
  const parsedInput = step.input.safeParse(opts.input);
131
131
  if (!parsedInput.success) throw new StepValidationError(step.name, "input", parsedInput.error);
132
- const collector = new StepObservabilityCollector({ liveEmitter: opts.liveAgentEventEmitter });
132
+ const collector = new StepObservabilityCollector({
133
+ liveEmitter: opts.liveAgentEventEmitter,
134
+ memoize: scopedMemoize(`step${opts.stepIndex}`),
135
+ });
133
136
  const coord = { runId: opts.run.id, stepIndex: opts.stepIndex };
134
137
  const pauseWithKind: KindedPauseFn = function <T>(req: PauseRequest<T>, kind: StepPauseRequest["kind"]): Promise<T> {
135
138
  return corePause(req, coord, kind);
@@ -148,7 +151,6 @@ export async function runWorkflowSingleStep(opts: RunWorkflowSingleStepOpts): Pr
148
151
  setMetadata: collector.setMetadata,
149
152
  step: collector.step,
150
153
  agentEvents: collector.agentEvents,
151
- checkpoint: scopedCheckpoint(`step${opts.stepIndex}`),
152
154
  pause,
153
155
  ...buildPauseWrappers(pauseWithKind),
154
156
  };
@@ -223,7 +225,10 @@ export async function runWorkflowSteps<TInput, TOutput>(
223
225
  throw err;
224
226
  }
225
227
 
226
- const collector = new StepObservabilityCollector({ liveEmitter: opts.liveAgentEventEmitter });
228
+ const collector = new StepObservabilityCollector({
229
+ liveEmitter: opts.liveAgentEventEmitter,
230
+ memoize: scopedMemoize(`step${i}`),
231
+ });
227
232
  const coord = { runId: run.id, stepIndex: i };
228
233
  const pauseWithKind: KindedPauseFn = function <T>(req: PauseRequest<T>, kind: StepPauseRequest["kind"]): Promise<T> {
229
234
  return corePause(req, coord, kind);
@@ -242,7 +247,6 @@ export async function runWorkflowSteps<TInput, TOutput>(
242
247
  setMetadata: collector.setMetadata,
243
248
  step: collector.step,
244
249
  agentEvents: collector.agentEvents,
245
- checkpoint: scopedCheckpoint(`step${i}`),
246
250
  pause,
247
251
  ...buildPauseWrappers(pauseWithKind),
248
252
  };
@@ -38,14 +38,18 @@ export interface StepContext<TInput = unknown> extends BaseExecutionContext {
38
38
  */
39
39
  setMetadata(data: Record<string, unknown>): Promise<void>;
40
40
  /**
41
- * Wrap a named sub-step for observability. Emits
42
- * `workflow_substep_started` / `workflow_substep_completed` /
43
- * `workflow_substep_failed` lifecycle events on the run timeline with
44
- * the measured `durationMs`. Use for long sub-phases inside one step
45
- * (setup, external API call, submit).
41
+ * Durable named sub-step (ADR-0012). Runs `fn` once and memoises its
42
+ * result to the state-dir checkpoint keyed `step<idx>.<name>`; on a
43
+ * pause-resume re-entry the value is read from disk and `fn` is NOT
44
+ * re-run. Emits `workflow_substep_started` /
45
+ * `workflow_substep_completed` / `workflow_substep_failed` lifecycle
46
+ * events on the run timeline with the measured `durationMs` — a restored
47
+ * sub-step reports `durationMs: 0` and status `"restored"`.
46
48
  *
47
- * The block runs even if observability flushing fails — instrumentation
48
- * never breaks the workflow.
49
+ * Names must be unique within one step body — they key the memoise file,
50
+ * so a reused name throws. A body that pauses (PauseSignal) is never
51
+ * memoised: the resume re-enters and re-runs it. For cross-process side
52
+ * effects (DB writes, emails) use `invokeChild`, not `step`.
49
53
  */
50
54
  step<T>(name: string, fn: () => Promise<T>): Promise<T>;
51
55
  /**
@@ -51,8 +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. */
54
+ /** Sandbox resources — machine SKU (`size`, a `SandboxSize` vCPU string)
55
+ * and `provider` (`vercel` | `e2b`). Vercel maps `size` to vCPUs; E2B
56
+ * sizing is template-defined. Omit → smallest SKU on the default provider. */
56
57
  resources?: SandboxResources;
57
58
  processors?: readonly Processor[];
58
59
  }