@agent-compose/cli 0.1.4 → 0.2.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/dist/index.js CHANGED
@@ -162,7 +162,7 @@ function makeClient({ url, apiKey }) {
162
162
  ` + " • Flag: --api-key YOUR_KEY");
163
163
  process.exit(1);
164
164
  }
165
- return new AgentComposeClient(url, apiKey);
165
+ return new AgentComposeClient({ apiKey, baseUrl: url });
166
166
  }
167
167
  function reportSdkError(err) {
168
168
  const msg = err instanceof AgentComposeError ? err.message : err instanceof Error ? err.message : String(err);
@@ -183,13 +183,13 @@ var registerCommand = new Command("register").description("Register a workflow w
183
183
  console.error(`Error: workflow not found at ${workflowPath}`);
184
184
  process.exit(1);
185
185
  }
186
- const { source, manifest, networkPolicy, placeholders, snapshot, saveSnapshot, memory, workflowPlan } = await bundleWorkflow(workflowPath);
186
+ const { source, manifest, networkPolicy, placeholders, snapshots, memory, postRunHooks, inputSchema, outputSchema, workflowPlan } = await bundleWorkflow(workflowPath);
187
187
  if (networkPolicy)
188
188
  console.log(`[register] Network policy detected — credentials will be brokered via Vercel firewall`);
189
- if (snapshot)
190
- console.log(`[register] Boots from snapshot: ${snapshot}`);
191
- if (saveSnapshot)
192
- console.log(`[register] Captures snapshot on success (saveSnapshot: true)`);
189
+ if (snapshots?.bootFrom)
190
+ console.log(`[register] Boots from: ${formatBootFrom(snapshots.bootFrom)}`);
191
+ if (snapshots?.saveLatest)
192
+ console.log(`[register] Captures snapshot per step${snapshots.retainSteps ? " (retain: every step)" : ""}`);
193
193
  const client = makeClient(opts);
194
194
  console.log(`[register] Registering "${name}" → factory "${opts.factory}"…`);
195
195
  const result = await client.register({
@@ -202,16 +202,23 @@ var registerCommand = new Command("register").description("Register a workflow w
202
202
  ...opts.schedule ? { schedule: opts.schedule } : {},
203
203
  ...networkPolicy ? { networkPolicy } : {},
204
204
  ...placeholders ? { placeholders } : {},
205
- ...snapshot ? { snapshot } : {},
206
- ...saveSnapshot !== undefined ? { saveSnapshot } : {},
207
- ...memory !== undefined ? { memory } : {}
205
+ ...snapshots !== undefined ? { snapshots } : {},
206
+ ...memory !== undefined ? { memory } : {},
207
+ ...postRunHooks !== undefined ? { postRunHooks } : {},
208
+ ...inputSchema !== undefined ? { inputSchema } : {},
209
+ ...outputSchema !== undefined ? { outputSchema } : {}
208
210
  });
209
211
  const scheduleNote = opts.schedule ? ` — schedule: ${opts.schedule}` : "";
210
212
  console.log(`✓ Workflow: ${result.name}@${result.version} (${result.id})${scheduleNote}`);
213
+ if (result.warnings && result.warnings.length > 0) {
214
+ for (const w of result.warnings) {
215
+ console.warn(`! warning: ${w}`);
216
+ }
217
+ }
211
218
  if (!opts.build)
212
219
  return;
213
- console.log(`[build] Invoking "${name}" with saveSnapshot:true to capture a snapshot…`);
214
- const { id: runId } = await client.invoke(name, {}, { saveSnapshot: true, factorySlug: opts.factory });
220
+ console.log(`[build] Invoking "${name}" with snapshots:{ saveLatest: true } to capture a snapshot…`);
221
+ const { id: runId } = await client.invoke(name, {}, { snapshots: { saveLatest: true }, factorySlug: opts.factory });
215
222
  console.log(`[build] Run: ${runId} — waiting for completion…`);
216
223
  const pollIntervalMs = 2000;
217
224
  const timeoutMinutes = opts.buildTimeout ?? 30;
@@ -220,7 +227,7 @@ var registerCommand = new Command("register").description("Register a workflow w
220
227
  while (Date.now() < deadline) {
221
228
  const { status } = await client.getStatus(runId);
222
229
  if (status === "success") {
223
- console.log(`✓ Snapshot ready — other workflows can now reference \`snapshot: "${name}"\``);
230
+ console.log(`✓ Snapshot ready — other workflows can now reference \`snapshots: { bootFrom: { workflow: "${name}" } }\``);
224
231
  return;
225
232
  }
226
233
  if (status === "failed" || status === "abandoned") {
@@ -232,6 +239,13 @@ var registerCommand = new Command("register").description("Register a workflow w
232
239
  console.error(`✗ Build timed out after ${timeoutMs / 60000}m (run ${runId} still running)`);
233
240
  process.exit(1);
234
241
  });
242
+ function formatBootFrom(b) {
243
+ if (b.snapshotId)
244
+ return `snapshot ${b.snapshotId}`;
245
+ if (b.workflow)
246
+ return b.version ? `${b.workflow}@${b.version}` : `${b.workflow} (latest)`;
247
+ return "(unknown)";
248
+ }
235
249
 
236
250
  // src/commands/invoke.ts
237
251
  import { Command as Command3 } from "commander";
@@ -658,7 +672,7 @@ keysCommand.command("create <name>").description("Create a new API key (requires
658
672
  process.exit(1);
659
673
  }
660
674
  }
661
- const client = new AgentComposeClient2(opts.url, opts.adminKey);
675
+ const client = new AgentComposeClient2({ apiKey: opts.adminKey, baseUrl: opts.url });
662
676
  const created = await client.createApiKey({
663
677
  name,
664
678
  ...scopes && { scopes },
@@ -682,7 +696,7 @@ API key created for "${name}":
682
696
  });
683
697
  keysCommand.command("list").description("List API keys for your team (requires admin-scoped ac_… key)").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--admin-key <key>", "Admin-scoped ac_… key", process.env.AGENT_COMPOSE_ADMIN_KEY ?? "").action(async (opts) => {
684
698
  requireAdminKey(opts.adminKey);
685
- const client = new AgentComposeClient2(opts.url, opts.adminKey);
699
+ const client = new AgentComposeClient2({ apiKey: opts.adminKey, baseUrl: opts.url });
686
700
  const data = await client.listApiKeys().catch(reportSdkError);
687
701
  if (!data.length) {
688
702
  console.log("No API keys.");
@@ -884,24 +898,49 @@ Usage ${from.toISOString().slice(0, 10)} → ${to.toISOString().slice(0, 10)}
884
898
  // src/commands/snapshot.ts
885
899
  import { Command as Command10 } from "commander";
886
900
  var snapshotCommand = new Command10("snapshot").description("Manage captured sandbox snapshots");
887
- snapshotCommand.command("list").description("List runs that have a captured sandbox snapshot").option("-w, --workflow <name>", "Filter to a single workflow").option("--limit <n>", "Max rows to show (default 50, max 500)", (v) => parseInt(v, 10)).option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (opts) => {
888
- const rows = await makeClient(opts).listSnapshots({
901
+ snapshotCommand.command("list").description("List captured sandbox snapshots in a factory").option("-w, --workflow <name>", "Filter to a single workflow").option("--limit <n>", "Max rows to show (default 50, max 500)", (v) => parseInt(v, 10)).option("--before <cursor>", "Pagination cursor from a previous page").option("--factory <slug>", "Factory slug", defaultFactory).option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (opts) => {
902
+ const page = await makeClient(opts).listSnapshotsPage({
903
+ factorySlug: opts.factory,
889
904
  ...opts.workflow ? { workflow: opts.workflow } : {},
890
- ...opts.limit != null ? { limit: opts.limit } : {}
905
+ ...opts.limit != null ? { limit: opts.limit } : {},
906
+ ...opts.before ? { before: opts.before } : {}
891
907
  });
908
+ const rows = page.data;
892
909
  if (rows.length === 0) {
893
910
  console.log("No captured snapshots.");
894
911
  return;
895
912
  }
896
913
  for (const r of rows) {
897
914
  const wfRef = r.workflow ? `${r.workflow}${r.version ? `@${r.version}` : ""}` : "-";
898
- const when = r.endedAt ?? "-";
899
- console.log(`${r.runId} ${wfRef} ${when} ${r.vercelSnapshotId}`);
915
+ const label = r.kind === "step" ? `step ${r.stepIndex}` : "latest";
916
+ const when = r.createdAt ?? "-";
917
+ console.log(`${r.runId} ${label.padEnd(8)} ${wfRef} ${when} ${r.snapshotId}`);
918
+ }
919
+ if (page.has_more && page.next_cursor) {
920
+ console.log(`next_cursor: ${page.next_cursor}`);
921
+ }
922
+ });
923
+ snapshotCommand.command("delete").description("Delete a captured snapshot. With <snapshot-id>, deletes that specific snapshot from the run; without, clears the run's latest snapshot pointer.").argument("<run-id>", "Run ID (UUID)").argument("[snapshot-id]", "Optional provider snapshot id (Vercel snapshot id)").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (runId, snapshotId, opts) => {
924
+ const client = makeClient(opts);
925
+ if (snapshotId) {
926
+ await client.deleteRunSnapshot(runId, snapshotId);
927
+ console.log(`Deleted snapshot ${snapshotId} from run ${runId}`);
928
+ } else {
929
+ await client.deleteSnapshot(runId);
930
+ console.log(`Deleted snapshot for run ${runId}`);
900
931
  }
901
932
  });
902
- snapshotCommand.command("delete").description("Delete the snapshot captured by a specific run").argument("<run-id>", "Run ID (UUID)").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (runId, opts) => {
903
- await makeClient(opts).deleteSnapshot(runId);
904
- console.log(`✓ Deleted snapshot for run ${runId}`);
933
+ snapshotCommand.command("show").description("List every snapshot held for a run (latest + retained step snapshots)").argument("<run-id>", "Run ID (UUID)").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (runId, opts) => {
934
+ const rows = await makeClient(opts).listRunSnapshots(runId);
935
+ if (rows.length === 0) {
936
+ console.log(`No snapshots for run ${runId}.`);
937
+ return;
938
+ }
939
+ for (const r of rows) {
940
+ const label = r.kind === "step" ? `step ${r.stepIndex}` : "latest";
941
+ const when = r.createdAt ?? "-";
942
+ console.log(`${label.padEnd(10)} ${when} ${r.snapshotId}`);
943
+ }
905
944
  });
906
945
 
907
946
  // src/commands/factory.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-compose/cli",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Command-line interface for agent-compose — register, invoke, and monitor workflows from your terminal.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -44,7 +44,7 @@
44
44
  "prepublishOnly": "bun run build"
45
45
  },
46
46
  "dependencies": {
47
- "@agent-compose/sdk": "^0.2.5",
47
+ "@agent-compose/sdk": "^0.3.0",
48
48
  "commander": "^12.0.0",
49
49
  "picocolors": "^1.1.1"
50
50
  },
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: ac:events
3
+ description: Send, list, and inspect events on agent-compose runs.
4
+ allowed-tools: Bash(agentc *)
5
+ effort: low
6
+ ---
7
+
8
+ # Events
9
+
10
+ Send events into an in-flight run, or list events from any run / the
11
+ whole factory. Events are structured, idempotent, propagating signals
12
+ authors emit alongside the agent loop — the dashboard's run timeline
13
+ renders them, downstream workflows subscribe to them, and analytics
14
+ queries scan them.
15
+
16
+ > Requires an API key with the `events:write` scope (or `invoke` for
17
+ > writes; reads are un-gated beyond authn). See `/ac:setup` to mint one.
18
+
19
+ ## When to use
20
+
21
+ - **Test a deployed workflow** — send a synthetic event to verify a
22
+ downstream subscriber fires.
23
+ - **Inspect what a run emitted** — list a run's full event stream when
24
+ the dashboard isn't open.
25
+ - **Audit factory traffic** — list factory-wide events filtered by name
26
+ or time range when debugging cross-workflow contracts.
27
+
28
+ ## Send an event
29
+
30
+ ```bash
31
+ # Simplest form — empty body, idempotent on (run, name) when --idempotency-key is set.
32
+ agentc events send <run-id> quality.accepted
33
+
34
+ # With body + summary + a single attribute:
35
+ agentc events send <run-id> deploy.completed \
36
+ --body '{"env":"prod","sha":"abc123"}' \
37
+ --summary "deploy to prod succeeded" \
38
+ -a region=us-east-1 \
39
+ -a duration_ms=42000
40
+
41
+ # Body from file:
42
+ agentc events send <run-id> review.done --body @review.json
43
+
44
+ # Replay-safe — re-running with the same key returns the original event id:
45
+ agentc events send <run-id> milestone --idempotency-key milestone:phase-2
46
+ ```
47
+
48
+ Useful flags:
49
+
50
+ | Flag | Notes |
51
+ |-------------------------|--------------------------------------------------------------------|
52
+ | `--body @file` | Inline JSON OR `@path/to/file` |
53
+ | `--summary "<text>"` | One-line human description (≤2000 chars) |
54
+ | `--confidence 0..1` | Useful for verification events |
55
+ | `-a key=value` (repeat) | Attributes. Values JSON-parsed when possible (`-a count=3` → number) |
56
+ | `--idempotency-key <k>` | (run, name, key) collapses to a single event |
57
+ | `--no-propagate` | Mark this event as terminal — don't fan out to subscribers |
58
+ | `--timestamp <iso>` | Override event time (defaults to server-now) |
59
+ | `--json` | Print the raw event JSON instead of the one-line summary |
60
+
61
+ ## List events
62
+
63
+ ```bash
64
+ # All events for a run:
65
+ agentc events list <run-id>
66
+
67
+ # Factory-wide (omit run id, add --factory):
68
+ agentc events list --factory default
69
+
70
+ # Filter by event name pattern:
71
+ agentc events list <run-id> --name "deploy.*"
72
+
73
+ # Page with --limit (default 50, max 500):
74
+ agentc events list <run-id> --limit 200
75
+ ```
76
+
77
+ ## Authoring events from inside a workflow
78
+
79
+ The CLI is for ad-hoc / testing use. Production workflows emit events via
80
+ `ctx.reportEvent(...)` (SDK):
81
+
82
+ ```ts
83
+ await ctx.reportEvent("deploy.completed", {
84
+ body: { env: "prod", sha },
85
+ summary: `deploy to prod (${sha.slice(0, 7)})`,
86
+ confidence: 1.0,
87
+ attributes: { region: "us-east-1" },
88
+ });
89
+ ```
90
+
91
+ ## Verify the events scope
92
+
93
+ ```bash
94
+ agentc keys list # confirm the active key has events:write
95
+ agentc events send <test-run> ping # if 403, the key is missing the scope
96
+ ```
97
+
98
+ Suggest `/ac:setup` if the key needs minting / rotating.
@@ -19,20 +19,26 @@ Interactively scaffold a new workflow from a plain-English description.
19
19
  ## Steps
20
20
 
21
21
  1. Ask the user:
22
- - What should this workflow do? (plain English — describe the
23
- phases, their inputs/outputs, and any external calls.)
22
+ - What should this workflow do? (plain English — describe the phases,
23
+ their inputs/outputs, and any external calls.)
24
24
  - Workflow name? (kebab-case, e.g. `code-review`)
25
25
  - Where to create it? (project-relative path, e.g. `src/workflows/`)
26
26
  - Runtime for agent loops? (default `claudeRuntime`)
27
- - Network policy needed? (which domains + which secrets to inject?)
28
- - Snapshot on success? (default off; on for setup/environment
27
+ - Network policy needed? (which domains + which brokered secrets to inject?)
28
+ - Capture a snapshot on success? (default off; on for setup/environment
29
29
  workflows whose end-state other workflows boot from)
30
+ - Boot this run from another workflow's snapshot? (default no — fresh
31
+ `node24` base sandbox)
32
+ - Enable the built-in memory extractor? (default off — opt in only when
33
+ this workflow's events are worth memorising AND the `workflow-memory`
34
+ template is registered in the same factory)
35
+ - Additional post-hooks? (ordered list of workflow names that fan out
36
+ after this run completes)
30
37
 
31
- 2. If the user has a reference workflow in their project, read it to
32
- match their conventions (prompt structure,
33
- error handling). Otherwise follow the patterns in
34
- [`sdk/README.md`](../../sdk/README.md) and the example in
35
- [`docs/how-it-works.md`](../../docs/how-it-works.md).
38
+ 2. If the user has a reference workflow in their project, read it to match
39
+ their conventions (prompt structure, error handling). Otherwise follow
40
+ the patterns in [`sdk/README.md`](../../sdk/README.md) and the example
41
+ in [`docs/how-it-works.md`](../../docs/how-it-works.md).
36
42
 
37
43
  3. Generate a complete, real implementation:
38
44
 
@@ -62,11 +68,43 @@ Interactively scaffold a new workflow from a plain-English description.
62
68
  await ctx.setMetadata({ summary: result.status?.summary });
63
69
  return { ok: !!result.status?.completed };
64
70
  },
65
- // Optional metadata picked up by the bundler at registration time:
66
- // networkPolicy: { allow: { /* */ } },
67
- // placeholders: { /* env-var formats for brokered secrets */ },
68
- // snapshot: "<other-workflow>", // boot from a snapshot
69
- // snapshot: true, // capture-on-success default
71
+
72
+ // ── Optional metadata picked up by the bundler at registration ──
73
+ //
74
+ // networkPolicy / placeholders outbound traffic + brokered secrets:
75
+ //
76
+ // networkPolicy: {
77
+ // allow: {
78
+ // "api.github.com": [{ transform: [{ headers: {
79
+ // Authorization: "basic:$GITHUB_TOKEN",
80
+ // } }] }],
81
+ // "*.openai.com": [{ transform: [{ headers: {
82
+ // "OpenAI-Beta": "$OPENAI_BETA_HEADER",
83
+ // } }] }],
84
+ // },
85
+ // },
86
+ // // Placeholder values the runner sees in env vars AFTER brokering.
87
+ // // The real secret never enters the VM — Vercel's firewall
88
+ // // substitutes it on the way out. Only needed when a tool /
89
+ // // SDK validates the env-var format on startup.
90
+ // placeholders: { GITHUB_TOKEN: "ghp_" + "x".repeat(36) },
91
+ //
92
+ // snapshots — all snapshot config in one object:
93
+ //
94
+ // snapshots: {
95
+ // // Boot from a specific captured snapshot. Pick the id from
96
+ // // `agentc snapshot list` or the factory snapshots page.
97
+ // bootFrom: { snapshotId: "snap_abc…" },
98
+ // saveLatest: true, // capture sandbox on success
99
+ // retainSteps: false, // keep one snapshot per successful step
100
+ // },
101
+ //
102
+ // memory — opt-in (default false). Requires `workflow-memory` to
103
+ // be registered in this factory.
104
+ // memory: true,
105
+ //
106
+ // postRunHooks — ordered list of workflow names that run after this:
107
+ // postRunHooks: ["audit-trail", "notify-slack"],
70
108
  });
71
109
  ```
72
110
 
@@ -82,3 +120,18 @@ Interactively scaffold a new workflow from a plain-English description.
82
120
 
83
121
  6. Tell the user: `agentc register <path>` to register, then
84
122
  `agentc invoke <name> --follow` to test.
123
+
124
+ ## When the user asks for memory extraction
125
+
126
+ The built-in memory extractor (`workflow-memory`) is a separate workflow
127
+ that must be registered in the same factory. Walk them through it:
128
+
129
+ 1. Copy the reference recipe from `templates/workflow-memory.ts` (or
130
+ `.agentc/smoketest/workflows/workflow-memory.ts` for the smoke
131
+ variant) into their project.
132
+ 2. `agentc register ./workflow-memory.ts` to install it.
133
+ 3. Set `memory: true` on the source workflow(s) — done.
134
+
135
+ If they want to skip the built-in and run custom post-run workflows,
136
+ use `postRunHooks: [...]` instead. Each post-hook is dispatched in order
137
+ with the source run's full context.
@@ -0,0 +1,128 @@
1
+ ---
2
+ name: ac:snapshots
3
+ description: List, inspect, and delete captured sandbox snapshots.
4
+ allowed-tools: Bash(agentc *)
5
+ effort: low
6
+ ---
7
+
8
+ # Snapshots
9
+
10
+ Sandbox snapshots are paid Vercel storage that does **not** auto-expire —
11
+ operators manage them by hand. This skill walks list/inspect/delete
12
+ through `agentc snapshot ...` for captured snapshots in one factory.
13
+
14
+ > **Mental model.** A snapshot is the on-disk state of a runner VM,
15
+ > captured at the end of a successful step. Other workflows boot from
16
+ > it via `snapshots: { bootFrom: { snapshotId: "snap_…" } }` — the
17
+ > snapshot id is the unit of identity. Operators pick a snapshot from
18
+ > the dashboard snapshot list (or `agentc snapshot list`), copy the id,
19
+ > and use it.
20
+
21
+ ## When to use
22
+
23
+ - **Find out what you're paying for** — list everything captured and
24
+ spot stale workflows nobody references.
25
+ - **Surface a snapshot id** to pin a workflow to a specific captured
26
+ state across deploys.
27
+ - **Free storage** by deleting snapshots you no longer need.
28
+
29
+ ## List captured snapshots
30
+
31
+ ```bash
32
+ # Everything captured by the active/default factory:
33
+ agentc snapshot list
34
+
35
+ # A specific factory:
36
+ agentc snapshot list --factory my-factory
37
+
38
+ # Filter to a single workflow:
39
+ agentc snapshot list --workflow my-setup
40
+
41
+ # Trim the page (default 50, max 500):
42
+ agentc snapshot list --limit 200
43
+
44
+ # Continue from a previous page:
45
+ agentc snapshot list --before <next_cursor>
46
+ ```
47
+
48
+ Output is one row per captured snapshot:
49
+
50
+ ```
51
+ <run-id> <capture> <workflow>@<version> <captured-at> <snapshot-id>
52
+ ```
53
+
54
+ The dashboard has a richer view at `/factories/<slug>/snapshots` — same
55
+ data plus size in bytes, capture kind (`latest` vs retained step), and
56
+ delete buttons. Use the CLI for scripting / quick triage.
57
+
58
+ ## Inspect a single run's snapshots
59
+
60
+ A run that opted into `snapshots: { saveLatest: true, retainSteps: true }`
61
+ ends up with one snapshot per successful step plus the latest pointer.
62
+ List them with:
63
+
64
+ ```bash
65
+ # All snapshots attached to a single run (latest + retained per-step):
66
+ agentc snapshot show <run-id>
67
+ ```
68
+
69
+ Each row shows step index (or "latest"), capture time, snapshot id, and
70
+ size.
71
+
72
+ ## Delete
73
+
74
+ ```bash
75
+ # Delete the run's latest snapshot pointer (legacy shorthand):
76
+ agentc snapshot delete <run-id>
77
+
78
+ # Delete a specific snapshot id from a run:
79
+ agentc snapshot delete <run-id> <snapshot-id>
80
+ ```
81
+
82
+ Once deleted, any workflow referencing that snapshot via
83
+ `bootFrom: { snapshotId }` gets a 503 at dispatch time. There's no
84
+ "latest of workflow X" fallback — the reference is to a specific
85
+ snapshot id, so it has to exist. Re-register the consuming workflow
86
+ with a different snapshot id (or re-capture).
87
+
88
+ ## Reference a snapshot from another workflow
89
+
90
+ ```ts
91
+ import { defineWorkflow } from "@agent-compose/sdk";
92
+
93
+ export default defineWorkflow({
94
+ // Boot from a captured snapshot. Pick the id from
95
+ // `agentc snapshot list` or the factory snapshots page.
96
+ snapshots: { bootFrom: { snapshotId: "snap_abc…" } },
97
+
98
+ async run(ctx, sandbox) {
99
+ // Sandbox boots in the captured state — no setup re-runs.
100
+ },
101
+ });
102
+ ```
103
+
104
+ ## Capture a snapshot from a workflow
105
+
106
+ ```ts
107
+ // On every successful run — captures the sandbox VM after the last step:
108
+ defineWorkflow({ snapshots: { saveLatest: true }, run: ... });
109
+
110
+ // Retain one per step (storage scales with step count):
111
+ defineWorkflow({ snapshots: { saveLatest: true, retainSteps: true }, run: ... });
112
+ ```
113
+
114
+ Per-invocation override:
115
+
116
+ ```bash
117
+ # Force capture for one specific run (overrides the registered default):
118
+ agentc invoke my-workflow --follow # no override
119
+ # (no CLI flag yet for snapshot capture; use the SDK or dashboard
120
+ # playground for per-invocation overrides.)
121
+ ```
122
+
123
+ ## Costs
124
+
125
+ Vercel snapshots: ~$0.08/GB-month (Pro/Enterprise pricing — confirm in
126
+ your plan). A 3 GB rootfs diff captured every 30s would burn ~$20k/month
127
+ in storage if every run kept one. Use `retainSteps` carefully on
128
+ high-frequency workflows.