@agent-compose/sdk 0.2.4 → 0.3.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.
package/src/client.ts CHANGED
@@ -17,6 +17,7 @@ import { parseSseStream } from "./sse.js";
17
17
  import type { SandboxNetworkPolicy } from "./sandbox.js";
18
18
  import type { RunEvent } from "./types/events.js";
19
19
  import type { WorkflowPlan } from "./types/workflow-plan.js";
20
+ import type { SnapshotConfig, IOSchema } from "./types/workflow-metadata.js";
20
21
  import type { WorkflowManifest } from "./utils/bundler.js";
21
22
 
22
23
  /** UUID-v4-ish — matches the server-side predicate. Used to auto-detect
@@ -51,6 +52,13 @@ export interface RegisterResult {
51
52
  name: string;
52
53
  version: string;
53
54
  runtimes?: RegisteredRuntime[];
55
+ /** Non-fatal advisories from the server. Surfaced at register time so the
56
+ * operator sees them while still in front of the terminal — currently
57
+ * covers "memory extraction is configured but its workflow is not
58
+ * registered in this factory". Empty/undefined when registration was
59
+ * cleanly resolved against everything the workflow declares it
60
+ * depends on. */
61
+ warnings?: string[];
54
62
  }
55
63
 
56
64
  export interface RegisteredRuntime {
@@ -79,14 +87,22 @@ export interface RegisterWorkflowInput {
79
87
  runtimes?: RuntimeSourceInput[];
80
88
  networkPolicy?: unknown;
81
89
  placeholders?: Record<string, string>;
82
- /** Reference to a snapshot the runner should boot from at run start. */
83
- snapshot?: string;
84
- /** If true, runs default to capturing a long-lived sandbox snapshot on success. */
85
- saveSnapshot?: boolean;
90
+ /** All snapshot config — `bootFrom` (where to restore at run start),
91
+ * `save`, `retain`. See `WorkflowMetadata.snapshots`. */
92
+ snapshots?: SnapshotConfig;
86
93
  /** Provider-neutral execution plan detected by the CLI bundler. */
87
94
  workflowPlan?: WorkflowPlan;
88
- /** Workflow Memory extraction config. Defaults to "default" when omitted. */
89
- memory?: "default" | false | { workflow: string };
95
+ /** Run the built-in memory extractor after this workflow completes.
96
+ * Opt-in; defaults to false when omitted. */
97
+ memory?: boolean;
98
+ /** Ordered list of workflow names that run as post-hooks after this
99
+ * workflow completes. The memory extractor (when `memory: true`)
100
+ * runs as an additional hook alongside these. */
101
+ postRunHooks?: readonly string[];
102
+ /** Input schema extracted from the workflow's `input` zod schema. */
103
+ inputSchema?: IOSchema;
104
+ /** Output schema extracted from the workflow's `output` zod schema. */
105
+ outputSchema?: IOSchema;
90
106
  /** Factory slug. Defaults to `"default"`. */
91
107
  factorySlug?: string;
92
108
  }
@@ -94,10 +110,11 @@ export interface RegisterWorkflowInput {
94
110
  export type RunState = "running" | "success" | "failed" | "abandoned" | "canceled";
95
111
 
96
112
  export interface InvokeWorkflowOptions {
97
- /** Per-invocation snapshot override: run UUID, workflow name, or `name@version`. */
98
- snapshot?: string;
99
- /** Per-invocation snapshot capture override. */
100
- saveSnapshot?: boolean;
113
+ /** Per-invocation snapshot config override. `snapshots.bootFrom`
114
+ * replaces the template's boot source; `snapshots.saveLatest` and
115
+ * `snapshots.retainSteps` override capture mode. Anything omitted
116
+ * falls back to the template's registered default. */
117
+ snapshots?: SnapshotConfig;
101
118
  /** Per-invocation network policy override. Replaces the template-level
102
119
  * policy for this run only — registered metadata is not mutated. */
103
120
  networkPolicy?: SandboxNetworkPolicy;
@@ -106,6 +123,13 @@ export interface InvokeWorkflowOptions {
106
123
  * vars after brokering. Replaces the template-level placeholders for
107
124
  * this run only — registered metadata is not mutated. */
108
125
  placeholders?: Record<string, string>;
126
+ /** Per-invocation memory-extractor override — `false` skips the
127
+ * built-in memory hook for this run; omitting leaves the registered
128
+ * default in place. */
129
+ memory?: boolean;
130
+ /** Per-invocation post-hook override — replaces the registered
131
+ * `postRunHooks` array for this run only. */
132
+ postRunHooks?: readonly string[];
109
133
  /** Explicit parent run id. Pass `null` to suppress ambient RUN_ID auto-detection. */
110
134
  parentRunId?: string | null;
111
135
  /** Agent loop inside the parent run that caused this invoke, when applicable. */
@@ -124,8 +148,12 @@ export interface InvokeResult {
124
148
  }
125
149
 
126
150
  export interface ListSnapshotsOptions {
151
+ /** Factory slug. Defaults to `"default"`. */
152
+ factorySlug?: string;
127
153
  workflow?: string;
128
154
  limit?: number;
155
+ /** Cursor returned as `next_cursor` from `listSnapshotsPage`. */
156
+ before?: string;
129
157
  }
130
158
 
131
159
  export interface TemplateRow {
@@ -332,19 +360,66 @@ export interface CancelRunResponse {
332
360
  }
333
361
 
334
362
  export interface SnapshotListEntry {
363
+ snapshotId: string;
364
+ kind: "latest" | "step";
365
+ stepIndex: number | null;
335
366
  runId: string;
336
367
  workflow: string | null;
337
368
  version: string | null;
338
- vercelSnapshotId: string;
339
- endedAt: string | null;
369
+ createdAt: string | null;
370
+ /** Provider-reported on-disk size, or null when unavailable. */
371
+ sizeBytes: number | null;
372
+ }
373
+
374
+ export interface SnapshotListResponse {
375
+ object: "list";
376
+ data: SnapshotListEntry[];
377
+ has_more: boolean;
378
+ next_cursor: string | null;
340
379
  }
341
380
 
381
+ export interface RunSnapshotEntry {
382
+ snapshotId: string;
383
+ /** `"latest"` = current/last pointer on the run.
384
+ * `"step"` = retained per-step snapshot. */
385
+ kind: "latest" | "step";
386
+ stepIndex: number | null;
387
+ createdAt: string | null;
388
+ /** Provider-reported on-disk size, or null when unavailable. */
389
+ sizeBytes: number | null;
390
+ }
391
+
392
+ export interface AgentComposeClientOptions {
393
+ /** Your team's API key — minted from the dashboard or `agentc keys create`.
394
+ * Required. Resolved from `process.env.AGENT_COMPOSE_API_KEY` when omitted. */
395
+ apiKey?: string;
396
+ /** Override the API host. Defaults to `process.env.AGENT_COMPOSE_URL` if
397
+ * set, otherwise the public host (`https://api.agentcompose.ai`). Most
398
+ * callers should leave this unset — the only consumers that need it are
399
+ * the CLI's local-dev path and tests that point at a fake server. */
400
+ baseUrl?: string;
401
+ }
402
+
403
+ const PUBLIC_API_HOST = "https://api.agentcompose.ai";
404
+
342
405
  export class AgentComposeClient {
343
406
  private readonly fetch: typeof ofetch;
344
407
  private readonly baseUrl: string;
345
408
  private readonly apiKey: string;
346
409
 
347
- constructor(baseUrl: string, apiKey: string) {
410
+ constructor(options: AgentComposeClientOptions = {}) {
411
+ const apiKey = options.apiKey
412
+ ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_API_KEY : undefined)
413
+ ?? "";
414
+ if (!apiKey) {
415
+ throw new Error(
416
+ "AgentComposeClient: `apiKey` is required. Pass it as `{ apiKey }` " +
417
+ "or set the AGENT_COMPOSE_API_KEY env var.",
418
+ );
419
+ }
420
+ const baseUrl = options.baseUrl
421
+ ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_URL : undefined)
422
+ ?? PUBLIC_API_HOST;
348
423
  this.baseUrl = baseUrl.replace(/\/$/, "");
349
424
  this.apiKey = apiKey;
350
425
  this.fetch = ofetch.create({
@@ -366,16 +441,9 @@ export class AgentComposeClient {
366
441
 
367
442
  /** Invoke a workflow. Returns run ID immediately — workflow runs asynchronously.
368
443
  *
369
- * `snapshot`: per-invocation override of the template-level `snapshot`
370
- * field. Same forms — a run UUID, a workflow name, or `name@version`.
371
- * Use this when one template needs to boot from many different
372
- * snapshots (e.g. a benchmark workflow running against a different
373
- * starting state per invocation). Does not change the template's
374
- * registered default; only affects this run.
375
- *
376
- * `saveSnapshot`: `true` overrides the workflow default; `false` opts
377
- * out. When the run captures a snapshot, its id is stamped on the row
378
- * and can be referenced as the `snapshot` field on other workflows.
444
+ * `snapshots`: per-invocation snapshot config override. `bootFrom`
445
+ * accepts `{ snapshotId }`; `saveLatest` / `retainSteps` control
446
+ * capture. Merged field-by-field with the registered default.
379
447
  *
380
448
  * `parentRunId`: links the new run to another of the same account's
381
449
  * currently-running runs. When omitted, the client auto-detects from
@@ -399,10 +467,11 @@ export class AgentComposeClient {
399
467
  method: "POST",
400
468
  body: {
401
469
  input,
402
- ...(opts?.snapshot !== undefined ? { snapshot: opts.snapshot } : {}),
403
- ...(opts?.saveSnapshot !== undefined ? { saveSnapshot: opts.saveSnapshot } : {}),
470
+ ...(opts?.snapshots !== undefined ? { snapshots: opts.snapshots } : {}),
404
471
  ...(opts?.networkPolicy !== undefined ? { networkPolicy: opts.networkPolicy } : {}),
405
472
  ...(opts?.placeholders !== undefined ? { placeholders: opts.placeholders } : {}),
473
+ ...(opts?.memory !== undefined ? { memory: opts.memory } : {}),
474
+ ...(opts?.postRunHooks !== undefined ? { postRunHooks: opts.postRunHooks } : {}),
406
475
  ...(parentRunId ? { parentRunId } : {}),
407
476
  ...(opts?.agentId ? { agentId: opts.agentId } : {}),
408
477
  },
@@ -441,20 +510,45 @@ export class AgentComposeClient {
441
510
  throw new AgentComposeError(504, `invokeAndWait: run ${runId} did not settle within ${timeoutMs}ms`);
442
511
  }
443
512
 
444
- /** List runs this account has captured snapshots for. */
513
+ /** List captured snapshots in a factory. */
445
514
  async listSnapshots(opts?: ListSnapshotsOptions): Promise<SnapshotListEntry[]> {
515
+ const body = await this.listSnapshotsPage(opts);
516
+ return body.data;
517
+ }
518
+
519
+ /** List one page of captured snapshots in a factory. */
520
+ async listSnapshotsPage(opts?: ListSnapshotsOptions): Promise<SnapshotListResponse> {
521
+ const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
446
522
  const q = new URLSearchParams();
447
523
  if (opts?.workflow) q.set("workflow", opts.workflow);
448
524
  if (opts?.limit != null) q.set("limit", String(opts.limit));
449
- const body = await this.fetch<{ object: "list"; data: SnapshotListEntry[]; has_more: boolean }>(
450
- `/api/v1/snapshots${q.toString() ? `?${q}` : ""}`,
525
+ if (opts?.before) q.set("before", opts.before);
526
+ return this.fetch<SnapshotListResponse>(
527
+ `/api/v1/factories/${encodeURIComponent(factorySlug)}/snapshots${q.toString() ? `?${q}` : ""}`,
451
528
  );
452
- return body.data;
453
529
  }
454
530
 
455
531
  /** Delete the snapshot captured by a specific run. Frees Vercel storage. */
456
532
  deleteSnapshot(runId: string): Promise<void> {
457
- return this.fetch(`/api/v1/workflows/${runId}/snapshot`, { method: "DELETE" });
533
+ return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/snapshot`, { method: "DELETE" });
534
+ }
535
+
536
+ /** List all snapshots captured for a single run — the latest pointer
537
+ * plus any retained per-step rows (`snapshots: { retainSteps: true }`). */
538
+ async listRunSnapshots(runId: string): Promise<RunSnapshotEntry[]> {
539
+ const body = await this.fetch<{ object: "list"; data: RunSnapshotEntry[]; has_more: boolean }>(
540
+ `/api/v1/workflows/${encodeURIComponent(runId)}/snapshots`,
541
+ );
542
+ return body.data;
543
+ }
544
+
545
+ /** Delete one snapshot from a run by id. Walks both the latest pointer
546
+ * and the retention rows; provider-side delete is best-effort. */
547
+ deleteRunSnapshot(runId: string, snapshotId: string): Promise<void> {
548
+ return this.fetch(
549
+ `/api/v1/workflows/${encodeURIComponent(runId)}/snapshots/${encodeURIComponent(snapshotId)}`,
550
+ { method: "DELETE" },
551
+ );
458
552
  }
459
553
 
460
554
  /** Poll run status. */
package/src/index.ts CHANGED
@@ -36,7 +36,15 @@ export type {
36
36
  WorkflowRun,
37
37
  AgentBudget,
38
38
  WorkflowHooks,
39
+ SnapshotConfig,
40
+ BootSnapshot,
41
+ IOSchema,
42
+ OutputSchema,
43
+ WorkflowMemoryConfig,
39
44
  } from "./types/workflow.js";
45
+
46
+ // Snapshot entry type re-exported for consumers (dashboard, CLI).
47
+ export type { RunSnapshotEntry } from "./client.js";
40
48
  export type { WorkflowPlan, WorkflowStepPlan } from "./types/workflow-plan.js";
41
49
  export type { BaseExecutionContext, InvokeChild } from "./types/execution-context.js";
42
50
 
@@ -105,7 +113,7 @@ export type {
105
113
  CreateApiKeyInput, StreamRunLogsOptions,
106
114
  EventSubjectType, EventRow, ReportEventInput, ListEventsOptions, ListEventsResult,
107
115
  RunLogLine, ListRunLogsOptions,
108
- RegisteredRuntime, RunState, RunStatus, FactoryRow, SnapshotListEntry,
116
+ RegisteredRuntime, RunState, RunStatus, FactoryRow, SnapshotListEntry, SnapshotListResponse,
109
117
  ApiKey, ApiKeyCreated,
110
118
  UsageRollupRow, UsageResponse,
111
119
  CancelRunResponse,
package/src/sandbox.ts CHANGED
@@ -239,7 +239,11 @@ function makeVercelSandboxProvider(sb: any, globalEnvs?: Record<string, string>)
239
239
  async snapshot() {
240
240
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
241
241
  const res: any = await sb.snapshot({ expiration: 0 });
242
- return { snapshotId: res.snapshotId as string };
242
+ const sizeBytes = typeof res.sizeBytes === "number" ? res.sizeBytes : undefined;
243
+ return {
244
+ snapshotId: res.snapshotId as string,
245
+ ...(sizeBytes !== undefined ? { sizeBytes } : {}),
246
+ };
243
247
  },
244
248
  };
245
249
  }
@@ -1,16 +1,14 @@
1
1
  /**
2
2
  * A sandbox environment is a workflow whose job is to leave its VM in a
3
3
  * configured state, then snapshot it so other workflows can boot from that
4
- * state. Capture is **opt-in**: the wrapper below sets `saveSnapshot: true`
5
- * by default (opt out with `saveSnapshot: false` if you only want side
6
- * effects).
4
+ * state. Capture is **opt-in**: the wrapper below sets
5
+ * `snapshots: { saveLatest: true }` by default (opt out with
6
+ * `saveLatest: false` if you only want side effects).
7
7
  *
8
- * Once captured, reference the snapshot from another workflow's `snapshot`
9
- * field by run UUID, workflow name, or `name@version`:
8
+ * Once captured, reference the snapshot from another workflow's
9
+ * `snapshots.bootFrom` field by provider snapshot id:
10
10
  *
11
- * snapshot: "my-setup" // most recent successful snapshot
12
- * snapshot: "my-setup@v1" // version-scoped recency
13
- * snapshot: "<run-uuid>" // pinned to an exact run
11
+ * snapshots: { bootFrom: { snapshotId: "snap_..." } }
14
12
  *
15
13
  * `defineSandboxEnvironment` is sugar over `defineWorkflow` — it makes the
16
14
  * setup recipe read like an imperative script by supplying the local
@@ -20,7 +18,7 @@
20
18
  * 1. Author a setup file with `defineSandboxEnvironment`.
21
19
  * 2. `agentc register setup.ts --build` — registers and invokes once to
22
20
  * capture the snapshot.
23
- * 3. Other workflows declare `snapshot: "name"` and boot from it.
21
+ * 3. Other workflows declare `snapshots: { bootFrom: { snapshotId } }`.
24
22
  * 4. `agentc snapshot list` / `delete` to manage the Vercel storage bill.
25
23
  *
26
24
  * @example
@@ -39,20 +37,31 @@
39
37
  import type { SandboxProvider } from "./sandbox.js";
40
38
  import { defineWorkflow } from "./workflow.js";
41
39
  import type { Workflow } from "../workflow-steps/types.js";
40
+ import type { SnapshotConfig } from "./workflow-metadata.js";
42
41
 
43
42
  export interface SandboxEnvironmentDefinition {
44
43
  name: string;
45
44
  description?: string;
46
45
  setup: (sb: SandboxProvider) => Promise<void>;
47
- /** Override the sugar's `saveSnapshot: true` default. Set `false` to opt
48
- * out of snapshot capture (rarely useful — an env with no snapshot can't
49
- * be referenced as the `snapshot` field on another workflow). */
50
- saveSnapshot?: boolean;
46
+ /** Override the sugar's `{ saveLatest: true }` default. Set
47
+ * `{ saveLatest: false }` to opt out of snapshot capture (rarely
48
+ * useful — an env with no snapshot can't be referenced as a
49
+ * `bootFrom` on another workflow). */
50
+ snapshots?: SnapshotConfig;
51
+ /** Override the sugar's `memory: false` default. Setup workflows
52
+ * don't typically benefit from memory extraction; opt in explicitly
53
+ * when they do. */
54
+ memory?: boolean;
51
55
  }
52
56
 
53
57
  /** Sugar over `defineWorkflow` for setup-only workflows that exist to
54
58
  * capture a snapshot. The workflow takes no meaningful input and returns
55
- * nothing — its value is the side effect on the sandbox VM. */
59
+ * nothing — its value is the side effect on the sandbox VM.
60
+ *
61
+ * Defaults `memory: false` because sandbox environments emit setup
62
+ * output (npm installs, command exit codes) rather than agent traces
63
+ * worth memorising. Authors can opt in explicitly via `env.memory:
64
+ * true` if their environment somehow does want extraction. */
56
65
  export function defineSandboxEnvironment(
57
66
  env: SandboxEnvironmentDefinition,
58
67
  ): Workflow<Record<string, unknown>, void> {
@@ -60,7 +69,8 @@ export function defineSandboxEnvironment(
60
69
  throw new Error(`defineSandboxEnvironment(${env.name}): 'setup' must be a function`);
61
70
  }
62
71
  return defineWorkflow<void, Record<string, unknown>>({
63
- saveSnapshot: env.saveSnapshot ?? true,
72
+ snapshots: env.snapshots ?? { saveLatest: true },
73
+ memory: env.memory ?? false,
64
74
  run: async (_ctx, sandbox) => env.setup(sandbox),
65
75
  });
66
76
  }
@@ -32,8 +32,12 @@ export interface SandboxProvider {
32
32
  /** Capture the running sandbox's state as a reusable snapshot. Vercel
33
33
  * supports it natively; E2B's model is Dockerfile-based and doesn't map
34
34
  * cleanly — `undefined` on providers that don't. Used by the server's
35
- * `--build` flow to stamp the snapshot id on the workflow row. */
36
- snapshot?(): Promise<{ snapshotId: string }>;
35
+ * `--build` flow to stamp the snapshot id on the workflow row.
36
+ *
37
+ * `sizeBytes` is the on-disk footprint reported by the provider. May
38
+ * be omitted when the provider doesn't expose it; the server stores
39
+ * `null` for missing values rather than estimating. */
40
+ snapshot?(): Promise<{ snapshotId: string; sizeBytes?: number }>;
37
41
  }
38
42
 
39
43
  /** Stateless provider-level snapshot deletion — no live sandbox needed,
@@ -20,18 +20,83 @@ import type { Processor } from "../processors/processor.js";
20
20
  * The bundler reads these from the default export at registration time
21
21
  * and forwards them to the server's POST /api/v1/templates payload.
22
22
  */
23
- /** Server-side knob for whether/how a Workflow Memory agent should run
24
- * after this workflow completes. `"default"` runs the built-in extractor;
25
- * `false` disables; an object names a custom memory workflow to dispatch. */
26
- export type WorkflowMemoryConfig = "default" | false | { workflow: string };
23
+ /** Whether the built-in Workflow Memory extractor should run after this
24
+ * workflow completes. Boolean toggle — custom post-run workflows live
25
+ * in the separate `postRunHooks` array on `WorkflowMetadata`. */
26
+ export type WorkflowMemoryConfig = boolean;
27
+
28
+ /** Where a run boots from. The snapshot id is the unit of identity —
29
+ * each captured snapshot already records the workflow + version it
30
+ * came from on the snapshot row, so there's no separate "latest of
31
+ * workflow X" resolution at dispatch time. Operators pick a snapshot
32
+ * from the dashboard snapshot list (or `agentc snapshot list`) and
33
+ * paste the id here.
34
+ *
35
+ * Omit `bootFrom` entirely to boot a fresh base sandbox. */
36
+ export type BootSnapshot = { snapshotId: string };
37
+
38
+ /** Snapshot configuration — boot source plus capture knobs. One object
39
+ * per workflow / per invocation; collapsing boot + capture under a
40
+ * single key reads as "all snapshot config lives here." */
41
+ export interface SnapshotConfig {
42
+ /** Where the runner restores from at run start. Structured (workflow
43
+ * ref or snapshot id) so the intent is explicit at the call site. */
44
+ bootFrom?: BootSnapshot;
45
+ /** Capture the sandbox state on terminal success. The latest pointer
46
+ * on `workflow_runs.vercel_snapshot_id` always tracks the most
47
+ * recent capture; without `retainSteps`, prior captures are deleted
48
+ * as new ones land — constant storage cost. */
49
+ saveLatest?: boolean;
50
+ /** Only meaningful with `saveLatest: true`. Retain every step's
51
+ * snapshot in `run_step_snapshots` so a future dispatch can boot
52
+ * from a specific step's checkpoint via its `snapshotId`. Cost
53
+ * scales linearly with step count. */
54
+ retainSteps?: boolean;
55
+ }
56
+
57
+ /** JSON-Schema-shaped description captured by the bundler from a
58
+ * workflow's `input` or `output` zod schema. Carried in template
59
+ * metadata so the dashboard can render typed input forms + output
60
+ * type tables. */
61
+ export interface IOSchema {
62
+ /** JSON Schema `type` keyword, when single-valued. Omitted for unions / unknowns. */
63
+ type?: string | string[];
64
+ /** Free-form description pulled from `.describe(...)` on the root. */
65
+ description?: string;
66
+ /** Object-shape: one entry per property. */
67
+ properties?: Record<string, { type?: string | string[]; description?: string }>;
68
+ /** Required property names — relevant only when `type === "object"`. */
69
+ required?: string[];
70
+ }
71
+
72
+ /** Alias kept for backwards source-compatibility with the original
73
+ * output-only release. New code should prefer `IOSchema`. */
74
+ export type OutputSchema = IOSchema;
27
75
 
28
76
  export interface WorkflowMetadata {
29
77
  networkPolicy?: SandboxNetworkPolicy;
30
78
  placeholders?: Record<string, string>;
31
- snapshot?: string;
32
- saveSnapshot?: boolean;
79
+ /** Input schema captured at bundle time from the workflow's
80
+ * declared `input` zod schema. Step-form workflows populate this
81
+ * automatically; run-form workflows (no explicit input schema —
82
+ * defaults to `z.unknown()`) leave it undefined. */
83
+ inputSchema?: IOSchema;
84
+ /** Output schema captured at bundle time from the workflow's
85
+ * declared `output` zod schema. Step-form workflows populate this
86
+ * automatically; run-form workflows whose `run()` returns
87
+ * arbitrarily-typed values leave it undefined. */
88
+ outputSchema?: IOSchema;
89
+ /** All snapshot config — boot source + capture mode. */
90
+ snapshots?: SnapshotConfig;
33
91
  processors?: readonly Processor[];
34
- memory?: WorkflowMemoryConfig;
92
+ /** Run the built-in memory extractor after this workflow completes.
93
+ * Opt-in; defaults to false when omitted. */
94
+ memory?: boolean;
95
+ /** Ordered list of workflow names that run after this workflow
96
+ * completes. The runtime dispatches them in declaration order; the
97
+ * built-in memory extractor (when `memory: true`) runs as a separate
98
+ * hook alongside whatever's declared here. */
99
+ postRunHooks?: readonly string[];
35
100
  }
36
101
 
37
102
  /**
@@ -54,12 +119,12 @@ export function extractMetadata(source: Partial<WorkflowMetadata>): WorkflowMeta
54
119
  const out: WorkflowMetadata = {};
55
120
  if (source.networkPolicy !== undefined) out.networkPolicy = freezeMetadataValue(source.networkPolicy);
56
121
  if (source.placeholders !== undefined) out.placeholders = Object.freeze({ ...source.placeholders });
57
- if (source.snapshot !== undefined) out.snapshot = source.snapshot;
58
- if (source.saveSnapshot !== undefined) out.saveSnapshot = source.saveSnapshot;
122
+ if (source.inputSchema !== undefined) out.inputSchema = freezeMetadataValue(source.inputSchema);
123
+ if (source.outputSchema !== undefined) out.outputSchema = freezeMetadataValue(source.outputSchema);
124
+ if (source.snapshots !== undefined) out.snapshots = Object.freeze({ ...source.snapshots });
59
125
  if (source.processors !== undefined) out.processors = Object.freeze([...source.processors]);
60
- if (source.memory !== undefined) out.memory = typeof source.memory === "object" && source.memory !== null
61
- ? Object.freeze({ ...source.memory })
62
- : source.memory;
126
+ if (source.memory !== undefined) out.memory = source.memory;
127
+ if (source.postRunHooks !== undefined) out.postRunHooks = Object.freeze([...source.postRunHooks]);
63
128
  return Object.freeze(out);
64
129
  }
65
130
 
@@ -37,8 +37,8 @@ export interface AgentEventSink {
37
37
  emit(event: AgentLifecycleEvent): void | Promise<void>;
38
38
  }
39
39
 
40
- import type { WorkflowMemoryConfig } from "./workflow-metadata.js";
41
- export type { WorkflowMemoryConfig };
40
+ import type { WorkflowMemoryConfig, SnapshotConfig, BootSnapshot, IOSchema, OutputSchema } from "./workflow-metadata.js";
41
+ export type { WorkflowMemoryConfig, SnapshotConfig, BootSnapshot, IOSchema, OutputSchema };
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. */
@@ -91,21 +91,21 @@ export interface WorkflowDefinition<
91
91
  > {
92
92
  run: WorkflowFn<TOutput, TInput>;
93
93
  /**
94
- * Reference to a snapshot the runner should boot from at run start.
95
- * Accepts a run UUID, a workflow name, or `name@version` — any workflow
96
- * registered with `--build` produces a snapshot you can name here. The
97
- * runner VM starts in that pre-configured state. Per-invocation
98
- * `invoke({ snapshot })` overrides this default.
99
- */
100
- snapshot?: string;
101
- /**
102
- * Capture a snapshot of the sandbox on successful /complete. Snapshots
103
- * are long-lived (never auto-expire) — customers list + delete them
104
- * explicitly via `agentc snapshot list/delete`. Per-invocation
105
- * `invoke({ saveSnapshot })` overrides this default.
106
- * `defineSandboxEnvironment` sugar sets this to true by default.
94
+ * All snapshot config — boot source plus capture mode.
95
+ *
96
+ * `snapshots.bootFrom`: the runner restores from this exact provider
97
+ * snapshot id at run start. Omit to boot a fresh sandbox.
98
+ *
99
+ * `snapshots.saveLatest`: `true` captures one snapshot after each
100
+ * successful step (latest-only — prior is freed).
101
+ * `{ saveLatest: true, retainSteps: true }` keeps every step's
102
+ * snapshot for fork / replay / time-travel.
103
+ *
104
+ * Snapshots are long-lived (never auto-expire). List + delete via
105
+ * `agentc snapshot list/delete`. Per-invocation
106
+ * `invoke({ snapshots })` overrides this default.
107
107
  */
108
- saveSnapshot?: boolean;
108
+ snapshots?: SnapshotConfig;
109
109
  /**
110
110
  * Outbound network policy for the runner sandbox.
111
111
  * Use "*": [] to allow all traffic while still injecting headers for specific domains.
@@ -142,10 +142,14 @@ export interface WorkflowDefinition<
142
142
  * }
143
143
  */
144
144
  placeholders?: Record<string, string>;
145
- /** Workflow Memory extraction. Defaults to "default" when omitted.
146
- * Set false to skip memory extraction for this workflow, or point at a
147
- * custom memory workflow once custom extractors are supported. */
145
+ /** Run the built-in memory extractor after this workflow completes.
146
+ * Opt-in; defaults to false when omitted. */
148
147
  memory?: WorkflowMemoryConfig;
148
+ /** Ordered list of workflow names to dispatch as post-hooks after
149
+ * this workflow completes. Each hook receives the source run's
150
+ * context. The memory extractor (when `memory: true`) runs as an
151
+ * additional hook alongside these. */
152
+ postRunHooks?: readonly string[];
149
153
  }
150
154
 
151
155
  /**