@agent-compose/cli 0.3.3 → 0.3.4

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
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/index.ts
6
- import { readFileSync as readFileSync6 } from "node:fs";
6
+ import { readFileSync as readFileSync7 } from "node:fs";
7
7
  import { fileURLToPath as fileURLToPath3 } from "node:url";
8
8
  import { program } from "commander";
9
9
 
@@ -147,6 +147,7 @@ function fallbackDashboardUrl(env) {
147
147
  return env === "local" ? "http://localhost:3000" : "https://platform.agentcompose.ai";
148
148
  }
149
149
  var defaultUrl = process.env.AGENT_COMPOSE_URL ?? _envUrl ?? _global.url ?? "https://api.agentcompose.ai";
150
+ var urlIsBuiltinFallback = !process.env.AGENT_COMPOSE_URL && !_envUrl && !_global.url;
150
151
  var defaultDashboardUrl = process.env.AGENT_COMPOSE_DASHBOARD_URL ?? _envDashboardUrl ?? _global.dashboardUrl ?? fallbackDashboardUrl(_activeEnv);
151
152
  var defaultApiKey = process.env.AGENT_COMPOSE_API_KEY ?? _envKey ?? _global.apiKey ?? "";
152
153
  var defaultFactory = process.env.AGENT_COMPOSE_FACTORY ?? _envFactory ?? "default";
@@ -155,6 +156,10 @@ var projectSettings = _project;
155
156
  var localSettings = _local;
156
157
  var activeEnv = _activeEnv;
157
158
  function makeClient({ url, apiKey }) {
159
+ if (urlIsBuiltinFallback && url === "https://api.agentcompose.ai") {
160
+ console.error(`[agentc] No server configured here — targeting https://api.agentcompose.ai (built-in default).
161
+ ` + " If you meant another environment: pass --url, set AGENT_COMPOSE_URL, or run from a repo with .agentc settings.");
162
+ }
158
163
  if (!apiKey) {
159
164
  console.error(`API key required. Provide via:
160
165
  ` + ` • Project: .agentc/settings.local.json
@@ -184,7 +189,9 @@ var registerCommand = new Command("register").description("Register a workflow w
184
189
  console.error(`Error: workflow not found at ${workflowPath}`);
185
190
  process.exit(1);
186
191
  }
187
- const { source, manifest, description, networkPolicy, placeholders, snapshots, memory, postRunHooks, inputSchema, outputSchema, workflowPlan } = await bundleWorkflow(workflowPath);
192
+ const { source, manifest, description, networkPolicy, placeholders, snapshots, inputSchema, outputSchema, workflowPlan, connectors, connectorOperation, invokePolicy } = await bundleWorkflow(workflowPath);
193
+ if (connectors)
194
+ console.log(`[register] Connectors required: ${Object.keys(connectors).join(", ")} — tokens are injected at the network layer at dispatch`);
188
195
  if (networkPolicy)
189
196
  console.log(`[register] Network policy detected — credentials will be brokered via Vercel firewall`);
190
197
  if (snapshots?.bootFrom)
@@ -205,18 +212,14 @@ var registerCommand = new Command("register").description("Register a workflow w
205
212
  ...networkPolicy ? { networkPolicy } : {},
206
213
  ...placeholders ? { placeholders } : {},
207
214
  ...snapshots !== undefined ? { snapshots } : {},
208
- ...memory !== undefined ? { memory } : {},
209
- ...postRunHooks !== undefined ? { postRunHooks } : {},
210
215
  ...inputSchema !== undefined ? { inputSchema } : {},
211
- ...outputSchema !== undefined ? { outputSchema } : {}
216
+ ...outputSchema !== undefined ? { outputSchema } : {},
217
+ ...connectors !== undefined ? { connectors } : {},
218
+ ...connectorOperation !== undefined ? { connectorOperation } : {},
219
+ ...invokePolicy !== undefined ? { invokePolicy } : {}
212
220
  });
213
221
  const scheduleNote = opts.schedule ? ` — schedule: ${opts.schedule}` : "";
214
222
  console.log(`✓ Workflow: ${result.name}@${result.version} (${result.id})${scheduleNote}`);
215
- if (result.warnings && result.warnings.length > 0) {
216
- for (const w of result.warnings) {
217
- console.warn(`! warning: ${w}`);
218
- }
219
- }
220
223
  if (!opts.build)
221
224
  return;
222
225
  console.log(`[build] Invoking "${name}" with snapshots:{ saveLatest: true } to capture a snapshot…`);
@@ -242,7 +245,7 @@ var registerCommand = new Command("register").description("Register a workflow w
242
245
  process.exit(1);
243
246
  });
244
247
  function formatBootFrom(b) {
245
- return `snapshot ${b.snapshotId}`;
248
+ return b === "reuse" ? "reuse (this workflow's own latest snapshot)" : `snapshot ${b.snapshotId}`;
246
249
  }
247
250
 
248
251
  // src/commands/invoke.ts
@@ -1213,11 +1216,38 @@ var listCommand2 = new Command13("list").description("List events for a run, or
1213
1216
  });
1214
1217
  var eventsCommand = new Command13("events").description("Send or list run events (Events API)").addCommand(sendCommand).addCommand(listCommand2);
1215
1218
 
1216
- // src/commands/schedule.ts
1219
+ // src/commands/files.ts
1220
+ import { readFileSync as readFileSync4, writeFileSync } from "node:fs";
1217
1221
  import { Command as Command14 } from "commander";
1218
- var sharedOpts2 = (cmd) => cmd.option("--factory <slug>", `Factory the schedule lives in (default: ${defaultFactory})`, defaultFactory).option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey);
1219
- var scheduleCommand = new Command14("schedule").description("Manage cron schedules attached to registered workflows");
1220
- sharedOpts2(scheduleCommand.command("create <name>").description("Create a new schedule for a workflow").requiredOption("--workflow <name>", "The registered workflow this schedule should fire").requiredOption("--cron <expr>", "Cron expression (UTC) — e.g. '0 9 * * *'")).action(async (name, opts) => {
1222
+ var sharedOpts2 = (cmd) => cmd.option("--factory <slug>", `Factory whose drive to target (default: ${defaultFactory})`, defaultFactory).option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey);
1223
+ var filesCommand = new Command14("files").description("Read/write documents on a factory's drive");
1224
+ sharedOpts2(filesCommand.command("put <local-file> <drive-path>").description("Upload a local file to the factory drive (creates or overwrites)").option("--content-type <mime>", "Content type", "text/plain; charset=utf-8")).action(async (localFile, drivePath, opts) => {
1225
+ const client = makeClient(opts);
1226
+ const result = await client.putFactoryFile(drivePath, readFileSync4(localFile), {
1227
+ factorySlug: opts.factory,
1228
+ contentType: opts.contentType
1229
+ });
1230
+ console.log(`✓ ${result.created ? "created" : "updated"} ${result.path} (${result.sizeBytes} bytes)`);
1231
+ });
1232
+ sharedOpts2(filesCommand.command("get <drive-path>").description("Print a drive file's content (or save with -o)").option("-o, --out <local-file>", "Write to a local file instead of stdout").option("--revision <n>", "Read a specific revision id")).action(async (drivePath, opts) => {
1233
+ const client = makeClient(opts);
1234
+ const content = await client.getFactoryFile(drivePath, {
1235
+ factorySlug: opts.factory,
1236
+ ...opts.revision !== undefined ? { revision: Number(opts.revision) } : {}
1237
+ });
1238
+ if (opts.out) {
1239
+ writeFileSync(opts.out, content);
1240
+ console.log(`✓ ${drivePath} → ${opts.out}`);
1241
+ } else {
1242
+ process.stdout.write(content);
1243
+ }
1244
+ });
1245
+
1246
+ // src/commands/schedule.ts
1247
+ import { Command as Command15 } from "commander";
1248
+ var sharedOpts3 = (cmd) => cmd.option("--factory <slug>", `Factory the schedule lives in (default: ${defaultFactory})`, defaultFactory).option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey);
1249
+ var scheduleCommand = new Command15("schedule").description("Manage cron schedules attached to registered workflows");
1250
+ sharedOpts3(scheduleCommand.command("create <name>").description("Create a new schedule for a workflow").requiredOption("--workflow <name>", "The registered workflow this schedule should fire").requiredOption("--cron <expr>", "Cron expression (UTC) — e.g. '0 9 * * *'")).action(async (name, opts) => {
1221
1251
  const client = makeClient(opts);
1222
1252
  const created = await client.createSchedule({
1223
1253
  name,
@@ -1227,7 +1257,7 @@ sharedOpts2(scheduleCommand.command("create <name>").description("Create a new s
1227
1257
  });
1228
1258
  console.log(`✓ Schedule "${created.name}" → workflow "${created.workflow}" @ "${created.cron}" (${created.id})`);
1229
1259
  });
1230
- sharedOpts2(scheduleCommand.command("list").description("List schedules in a factory")).action(async (opts) => {
1260
+ sharedOpts3(scheduleCommand.command("list").description("List schedules in a factory")).action(async (opts) => {
1231
1261
  const client = makeClient(opts);
1232
1262
  const rows = await client.listSchedules(opts.factory);
1233
1263
  if (rows.length === 0) {
@@ -1240,21 +1270,21 @@ sharedOpts2(scheduleCommand.command("list").description("List schedules in a fac
1240
1270
  console.log(` ${s.name.padEnd(24)} ${s.workflowName.padEnd(20)} ${s.cron.padEnd(16)} next: ${next} ${s.id}`);
1241
1271
  }
1242
1272
  });
1243
- sharedOpts2(scheduleCommand.command("delete <id>").description("Delete a schedule by id")).action(async (id, opts) => {
1273
+ sharedOpts3(scheduleCommand.command("delete <id>").description("Delete a schedule by id")).action(async (id, opts) => {
1244
1274
  const client = makeClient(opts);
1245
1275
  await client.deleteSchedule(id, opts.factory);
1246
1276
  console.log(`✓ Schedule ${id} deleted from factory "${opts.factory}"`);
1247
1277
  });
1248
1278
 
1249
1279
  // src/commands/upgrade.ts
1250
- import { Command as Command15 } from "commander";
1280
+ import { Command as Command16 } from "commander";
1251
1281
  import { spawnSync } from "node:child_process";
1252
- import { readFileSync as readFileSync4 } from "node:fs";
1282
+ import { readFileSync as readFileSync5 } from "node:fs";
1253
1283
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1254
1284
  import pc4 from "picocolors";
1255
1285
  var PACKAGE_NAME = "@agent-compose/cli";
1256
- var upgradeCommand = new Command15("upgrade").description("Update the agentc CLI to the latest published version").option("--check", "Check for updates without installing", false).action(async (opts) => {
1257
- const pkg = JSON.parse(readFileSync4(fileURLToPath2(new URL("../../package.json", import.meta.url)), "utf8"));
1286
+ var upgradeCommand = new Command16("upgrade").description("Update the agentc CLI to the latest published version").option("--check", "Check for updates without installing", false).action(async (opts) => {
1287
+ const pkg = JSON.parse(readFileSync5(fileURLToPath2(new URL("../../package.json", import.meta.url)), "utf8"));
1258
1288
  const current = pkg.version;
1259
1289
  const controller = new AbortController;
1260
1290
  const timeout = setTimeout(() => controller.abort(), 5000);
@@ -1296,7 +1326,7 @@ var upgradeCommand = new Command15("upgrade").description("Update the agentc CLI
1296
1326
 
1297
1327
  // src/update-check.ts
1298
1328
  import { spawnSync as spawnSync2 } from "node:child_process";
1299
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync } from "node:fs";
1329
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
1300
1330
  import { homedir as homedir3 } from "node:os";
1301
1331
  import { join as join3 } from "node:path";
1302
1332
  import { createInterface } from "node:readline/promises";
@@ -1336,7 +1366,7 @@ function isNewer(latest, current) {
1336
1366
  }
1337
1367
  function readCache() {
1338
1368
  try {
1339
- const raw = readFileSync5(cachePath(), "utf8");
1369
+ const raw = readFileSync6(cachePath(), "utf8");
1340
1370
  const parsed = JSON.parse(raw);
1341
1371
  if (typeof parsed.checkedAt !== "number")
1342
1372
  return null;
@@ -1352,7 +1382,7 @@ function writeCache(entry) {
1352
1382
  try {
1353
1383
  const path = cachePath();
1354
1384
  mkdirSync2(join3(path, ".."), { recursive: true });
1355
- writeFileSync(path, JSON.stringify(entry, null, 2) + `
1385
+ writeFileSync2(path, JSON.stringify(entry, null, 2) + `
1356
1386
  `, "utf8");
1357
1387
  } catch {}
1358
1388
  }
@@ -1425,7 +1455,7 @@ async function checkForUpdate(currentVersion) {
1425
1455
  }
1426
1456
 
1427
1457
  // src/index.ts
1428
- var pkg = JSON.parse(readFileSync6(fileURLToPath3(new URL("../package.json", import.meta.url)), "utf8"));
1458
+ var pkg = JSON.parse(readFileSync7(fileURLToPath3(new URL("../package.json", import.meta.url)), "utf8"));
1429
1459
  program.name("agentc").description("Coordinate agent production lines.").version(pkg.version);
1430
1460
  program.hook("preAction", async () => {
1431
1461
  try {
@@ -1439,6 +1469,7 @@ program.addCommand(cancelCommand);
1439
1469
  program.addCommand(scheduleCommand);
1440
1470
  program.addCommand(logsCommand);
1441
1471
  program.addCommand(eventsCommand);
1472
+ program.addCommand(filesCommand);
1442
1473
  program.addCommand(factoryCommand);
1443
1474
  program.addCommand(snapshotCommand);
1444
1475
  program.addCommand(secretsCommand);
@@ -1452,6 +1483,7 @@ Topics:
1452
1483
  Workflows register, invoke, list, cancel
1453
1484
  Scheduling schedule
1454
1485
  Run inspection logs, events
1486
+ Factory drive files
1455
1487
  Resources factory, snapshot, secrets
1456
1488
  Account keys, auth, usage
1457
1489
  Setup init, upgrade
@@ -1461,6 +1493,7 @@ Examples:
1461
1493
  $ agentc invoke pipeline --follow
1462
1494
  $ agentc schedule create nightly --workflow pipeline --cron '0 9 * * *'
1463
1495
  $ agentc logs <run-id>
1496
+ $ agentc files put report.md runs/abc123/report.md
1464
1497
 
1465
1498
  Docs:
1466
1499
  https://github.com/Layr-Labs/agent-compose
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-compose/cli",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Command-line interface for agent-compose — register, invoke, and monitor workflows from your terminal.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: ac:files
3
+ description: Read and write documents on a factory's drive from the CLI.
4
+ allowed-tools: Bash(agentc *)
5
+ effort: low
6
+ ---
7
+
8
+ # Files
9
+
10
+ Put files onto a factory's shared drive and read them back — all through
11
+ `agentc`, no raw HTTP. The drive is where agent-written documents, briefs,
12
+ and run artifacts live; the dashboard's **Files** tab renders them, and
13
+ writes are indexed (revisions, search, attribution). Two subcommands:
14
+ `agentc files put` and `agentc files get`.
15
+
16
+ The primary consumer is an **agent working inside a run sandbox**: publishing
17
+ work to the dashboard while the run is going. Inside a sandbox the env already
18
+ carries `AGENT_COMPOSE_URL` / `AGENT_COMPOSE_API_KEY` / `AGENT_COMPOSE_FACTORY`,
19
+ and the CLI attaches the run-callback token automatically so the write is
20
+ **attributed to the run** (it shows on the file and in the run's Artifacts
21
+ card). Works identically from a dev machine with saved credentials.
22
+
23
+ > Requires an API key with `invoke` (writes) / `read` (reads). See `/ac:setup`.
24
+
25
+ ## When to use
26
+
27
+ - **Publish run output** — an agent saves a report/brief so a human sees it
28
+ in the dashboard mid-run.
29
+ - **Share between runs** — write a file other runs of the same factory read.
30
+ - **Pull a file locally** — fetch a drive document to inspect or edit.
31
+
32
+ ## Upload a file
33
+
34
+ ```bash
35
+ # Create or overwrite a drive file from a local file.
36
+ # Convention: agents write under runs/<short-run-id>/...
37
+ agentc files put report.md runs/abc123/report.md
38
+
39
+ # Override the content type (default text/plain; charset=utf-8):
40
+ agentc files put index.html sites/demo/index.html --content-type "text/html; charset=utf-8"
41
+
42
+ # Target a non-default factory:
43
+ agentc files put notes.md briefs/notes.md --factory my-factory
44
+ ```
45
+
46
+ ## Download / read a file
47
+
48
+ ```bash
49
+ # Print a drive file to stdout:
50
+ agentc files get briefs/site-7b7954fc.md
51
+
52
+ # Save to a local file:
53
+ agentc files get runs/abc123/report.md -o ./report.md
54
+
55
+ # Read a specific historical revision:
56
+ agentc files get briefs/notes.md --revision 12
57
+ ```
58
+
59
+ ## Browsing the drive
60
+
61
+ There is no `agentc files list` — browse via the dashboard's **Files** tab,
62
+ or (inside a sandbox whose factory has a provisioned Archil disk)
63
+ `ls /factory` on the POSIX mount.
64
+
65
+ ## Authoring from inside a workflow
66
+
67
+ The CLI is for agents + ad-hoc use. Workflow code uses the SDK, which attaches
68
+ the run token the same way:
69
+
70
+ ```ts
71
+ import { AgentComposeClient } from "@agent-compose/sdk";
72
+ const client = new AgentComposeClient({ apiKey, baseUrl });
73
+ await client.putFactoryFile("runs/abc123/report.md", contents, {
74
+ contentType: "text/markdown; charset=utf-8",
75
+ });
76
+ ```
77
+
78
+ ## The drive vs. /factory mount
79
+
80
+ If the factory has a provisioned Archil disk, the drive is also POSIX-mounted
81
+ at `/factory` inside sandboxes — fine for reading and for scratch shared
82
+ between sibling runs. But a direct `/factory` write is **not indexed** (no
83
+ revision/attribution), so to make a file appear in the dashboard Files tab use
84
+ `agentc files put` (or the SDK), which goes through the indexed files API.
@@ -60,7 +60,7 @@ about "step-form vs run-form" — that's an internal call.
60
60
  - step-form vs run-form
61
61
  - sandbox environments / dependency installation
62
62
  - snapshots: `saveLatest`, `retainSteps`, `bootFrom`
63
- - `memory` / `postRunHooks` / `processors`
63
+ - `processors`
64
64
 
65
65
  Decide those internally based on what they described (see the
66
66
  "Internal decisions" section below).
@@ -69,28 +69,44 @@ Decide those internally based on what they described (see the
69
69
 
70
70
  After answering the questions above, decide the shape WITHOUT asking:
71
71
 
72
- ### Pick the workflow shape
73
-
74
- - **Step-form** (`defineWorkflow({...}).step(s1).step(s2).build()` with
75
- `defineStep(...)` objects) pick this when the body decomposes
76
- cleanly into named phases with typed handoffs. Each step's output
77
- threads into the next step's input. The dashboard renders each step
78
- as a typed phase with its own duration. Examples: ETL pipelines,
79
- classification enrichment publish, fetch score rank.
80
-
81
- - **Run-form** (one `async (ctx, sandbox)` body with `agent({...})`
82
- calls inside) pick this when the body is dominated by one or more
83
- agent loops. Decomposing an LLM iteration into typed engine steps is
84
- the wrong shape. Use `ctx.step("phase-name", () => …)` inside the
85
- body for observability sub-events when there's pre-agent setup
86
- worth tracing on the run timeline.
87
-
88
- When the user's description says "agent", "LLM", "the model decides",
89
- "reasons through", "writes a summary based on", "navigates the
90
- website" that's run-form. When they describe deterministic phases
91
- that each transform structured data — that's step-form. If genuinely
92
- mixed, default to run-form and use `ctx.step` for the deterministic
93
- phases.
72
+ ### Pick the workflow shape — DEFAULT TO STEP-FORM
73
+
74
+ **Default to step-form, even for agent-driven workflows.** Each meaningful
75
+ phaseincluding an agent pass becomes a `defineStep(...)`, and
76
+ `snapshots: { saveLatest: true }` makes the engine capture a snapshot AFTER
77
+ EACH step (latest-only). That durability is the whole point: if a later step's
78
+ sandbox dies, or a step needs retrying, the engine resumes from the last
79
+ completed step's snapshot instead of re-running the entire (often expensive,
80
+ multi-agent) pipeline from the top.
81
+
82
+ A multi-phase agent pipeline written **run-form is ONE engine step** a single
83
+ snapshot at the very end so ANY failure throws away all the work and restarts
84
+ at phase one. That's the brittleness step-form avoids.
85
+
86
+ - **Step-form** (`defineWorkflow({ id, input, output, snapshots: { saveLatest: true } }).step(s1).step(s2).build()`
87
+ with `defineStep(...)` objects) — the default. Each phase (fetch, an agent
88
+ pass, a transform, a deploy) is a step. **Steps DO receive a `sandbox`** and
89
+ run `agent({ sandbox: ctx.sandbox, })`, `ctx.sandbox.commands.run(…)`, etc.
90
+ they are NOT limited to pure data transforms. Each step's output threads
91
+ into the next step's input. The dashboard renders each as a typed,
92
+ separately-timed, separately-snapshotted phase. Examples: content imagery
93
+ polish → verify → deploy; fetch → score → rank → publish.
94
+
95
+ - **Run-form** (one `async (ctx, sandbox)` body) — LEGACY; being phased out in
96
+ favor of step-form everywhere. Only acceptable when the work is a SINGLE
97
+ indivisible agent loop with no meaningful phase boundaries. You give up
98
+ per-step snapshots (one capture at the very end). Use `ctx.step("phase", () => …)`
99
+ inside for timeline observability — it is NOT a snapshot boundary.
100
+ ⚠️ Replay semantics: a paused run-form workflow RESUMES BY RE-EXECUTING the
101
+ whole function from the top — only `ctx.requestDecision` results are
102
+ memoized. Every pre-pause side effect re-fires on resume, so writes must be
103
+ create-only/non-clobbering (an unconditional re-write destroyed a human's
104
+ pause-time edits in production testing). If the workflow pauses at all,
105
+ author it step-form: completed steps structurally never replay.
106
+
107
+ If the work has more than one phase — especially multiple agent passes, or any
108
+ step whose work you'd hate to lose on a failure — it's step-form. Each such
109
+ phase is its own step.
94
110
 
95
111
  ### Should it have a sandbox environment?
96
112
 
@@ -196,6 +212,7 @@ export default defineWorkflow({
196
212
  description: "Pulls open PRs from a GitHub repo, scores each one by review urgency, surfaces the top five.",
197
213
  input: InputSchema,
198
214
  output: OutputSchema,
215
+ snapshots: { saveLatest: true }, // snapshot after each step → resume from the last one, not the top
199
216
  })
200
217
  .step(fetchStep)
201
218
  .step(scoreStep)
@@ -206,10 +223,18 @@ export default defineWorkflow({
206
223
  Notes:
207
224
  - Each step's `output` schema must satisfy the next step's `input`
208
225
  schema (the SDK enforces this at `defineWorkflow().step(...)` time
209
- via TS inference). Reshape inside the upstream step's `run` body,
210
- not at the boundary.
211
- - Step bodies don't receive a `sandbox`. If a step needs to run code
212
- inside the runner VM, that's the agent-driven shape use run-form.
226
+ via TS inference), and the FINAL step's `output` must be the same
227
+ zod instance as the workflow's declared `output`. Reshape inside the
228
+ upstream step's `run` body, not at the boundary.
229
+ - **Step bodies DO receive a `sandbox`**`ctx.sandbox` is present
230
+ whenever the run executes in a sandbox-backed workspace. Run
231
+ `agent({ sandbox: ctx.sandbox, … })`, `ctx.sandbox.commands.run(…)`,
232
+ `ctx.sandbox.files.write(…)` inside any step. (The example above uses
233
+ pure transforms, but an agent pass is a perfectly good step — and the
234
+ preferred shape, because each step is independently snapshotted.)
235
+ - `snapshots: { saveLatest: true }` captures the sandbox after EACH
236
+ step (latest-only); add `retainSteps: true` only if you need every
237
+ step's snapshot kept for fork/replay (linear storage cost).
213
238
 
214
239
  ### Template B — run-form (agent-driven body)
215
240
 
@@ -281,13 +306,6 @@ export default defineWorkflow({
281
306
  // saveLatest: true,
282
307
  // retainSteps: false,
283
308
  // },
284
- //
285
- // memory — opt-in. Requires `workflow-memory` to be registered in
286
- // this factory.
287
- // memory: true,
288
- //
289
- // postRunHooks — workflows that run after this one completes:
290
- // postRunHooks: ["audit-trail", "notify-slack"],
291
309
  });
292
310
  ```
293
311
 
@@ -347,20 +365,3 @@ export default defineWorkflow({
347
365
  "To run it on the schedule: `agentc schedule create pr-triage-daily
348
366
  --workflow pr-triage --cron '<expr>'` — the cron pattern we
349
367
  worked out earlier.")
350
-
351
- ## When the user asks for memory extraction
352
-
353
- The built-in memory extractor (`workflow-memory`) is a separate
354
- workflow that must be registered in the same factory. Walk them
355
- through it in user terms:
356
-
357
- 1. "I'll add the `workflow-memory` recipe to your project."
358
- Copy from `.agentc/templates/workflow-memory.ts`.
359
- 2. "Run `agentc register ./workflow-memory.ts` once to install it."
360
- 3. "Now any workflow with `memory: true` will trigger it after each
361
- successful run."
362
-
363
- For custom post-run workflows (analytics, audit, notifications)
364
- without the built-in extractor, use `postRunHooks: [...]` instead.
365
- Each post-hook runs in declaration order with the source run's full
366
- context.