@agent-compose/cli 0.3.2 → 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
 
@@ -137,7 +137,8 @@ async function saveLocalSettings(projectDir, settings) {
137
137
  var [_project, _projectDir] = findProjectRoot();
138
138
  var _local = loadLocalSettings(_projectDir);
139
139
  var _global = loadGlobalSettings();
140
- var _activeEnv = _local.env ?? (_project.envs?.["prod"] ? "prod" : undefined);
140
+ var _candidateEnv = _local.env ?? _project.defaultEnv ?? "prod";
141
+ var _activeEnv = _project.envs?.[_candidateEnv] ? _candidateEnv : undefined;
141
142
  var _envUrl = _activeEnv ? _project.envs?.[_activeEnv]?.url : undefined;
142
143
  var _envDashboardUrl = _activeEnv ? _project.envs?.[_activeEnv]?.dashboardUrl : undefined;
143
144
  var _envKey = _activeEnv ? _local.keys?.[_activeEnv] : undefined;
@@ -146,6 +147,7 @@ function fallbackDashboardUrl(env) {
146
147
  return env === "local" ? "http://localhost:3000" : "https://platform.agentcompose.ai";
147
148
  }
148
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;
149
151
  var defaultDashboardUrl = process.env.AGENT_COMPOSE_DASHBOARD_URL ?? _envDashboardUrl ?? _global.dashboardUrl ?? fallbackDashboardUrl(_activeEnv);
150
152
  var defaultApiKey = process.env.AGENT_COMPOSE_API_KEY ?? _envKey ?? _global.apiKey ?? "";
151
153
  var defaultFactory = process.env.AGENT_COMPOSE_FACTORY ?? _envFactory ?? "default";
@@ -154,6 +156,10 @@ var projectSettings = _project;
154
156
  var localSettings = _local;
155
157
  var activeEnv = _activeEnv;
156
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
+ }
157
163
  if (!apiKey) {
158
164
  console.error(`API key required. Provide via:
159
165
  ` + ` • Project: .agentc/settings.local.json
@@ -183,7 +189,9 @@ var registerCommand = new Command("register").description("Register a workflow w
183
189
  console.error(`Error: workflow not found at ${workflowPath}`);
184
190
  process.exit(1);
185
191
  }
186
- 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`);
187
195
  if (networkPolicy)
188
196
  console.log(`[register] Network policy detected — credentials will be brokered via Vercel firewall`);
189
197
  if (snapshots?.bootFrom)
@@ -204,18 +212,14 @@ var registerCommand = new Command("register").description("Register a workflow w
204
212
  ...networkPolicy ? { networkPolicy } : {},
205
213
  ...placeholders ? { placeholders } : {},
206
214
  ...snapshots !== undefined ? { snapshots } : {},
207
- ...memory !== undefined ? { memory } : {},
208
- ...postRunHooks !== undefined ? { postRunHooks } : {},
209
215
  ...inputSchema !== undefined ? { inputSchema } : {},
210
- ...outputSchema !== undefined ? { outputSchema } : {}
216
+ ...outputSchema !== undefined ? { outputSchema } : {},
217
+ ...connectors !== undefined ? { connectors } : {},
218
+ ...connectorOperation !== undefined ? { connectorOperation } : {},
219
+ ...invokePolicy !== undefined ? { invokePolicy } : {}
211
220
  });
212
221
  const scheduleNote = opts.schedule ? ` — schedule: ${opts.schedule}` : "";
213
222
  console.log(`✓ Workflow: ${result.name}@${result.version} (${result.id})${scheduleNote}`);
214
- if (result.warnings && result.warnings.length > 0) {
215
- for (const w of result.warnings) {
216
- console.warn(`! warning: ${w}`);
217
- }
218
- }
219
223
  if (!opts.build)
220
224
  return;
221
225
  console.log(`[build] Invoking "${name}" with snapshots:{ saveLatest: true } to capture a snapshot…`);
@@ -241,7 +245,7 @@ var registerCommand = new Command("register").description("Register a workflow w
241
245
  process.exit(1);
242
246
  });
243
247
  function formatBootFrom(b) {
244
- return `snapshot ${b.snapshotId}`;
248
+ return b === "reuse" ? "reuse (this workflow's own latest snapshot)" : `snapshot ${b.snapshotId}`;
245
249
  }
246
250
 
247
251
  // src/commands/invoke.ts
@@ -257,8 +261,15 @@ function fmt(ms) {
257
261
  return `${Math.floor(ms / 60000)}m ${Math.floor(ms % 60000 / 1000)}s`;
258
262
  }
259
263
  function extractToolSnippet(toolInput) {
260
- const raw = toolInput ? String(toolInput) : "";
261
- return raw.match(/"(?:command|file_path|url|query|pattern)":"([^"]{1,80})/)?.[1] ?? "";
264
+ if (!toolInput || typeof toolInput !== "object")
265
+ return "";
266
+ const input = toolInput;
267
+ for (const key of ["command", "file_path", "url", "query", "pattern"]) {
268
+ const value = input[key];
269
+ if (typeof value === "string" && value.length > 0)
270
+ return value.slice(0, 80);
271
+ }
272
+ return "";
262
273
  }
263
274
  function printAgentMessage(data) {
264
275
  const message = data.message;
@@ -280,7 +291,7 @@ function printAgentMessage(data) {
280
291
  break;
281
292
  }
282
293
  case "tool_use": {
283
- const detail = extractToolSnippet(message.toolInputPreview);
294
+ const detail = extractToolSnippet(message.toolInput);
284
295
  process.stdout.write(` ${pc.cyan("→")} ${pc.bold(String(message.toolName ?? "tool"))}${detail ? ` ${pc.dim(detail)}` : ""}
285
296
  `);
286
297
  break;
@@ -291,6 +302,15 @@ function printAgentMessage(data) {
291
302
  break;
292
303
  }
293
304
  }
305
+ function printConsoleLine(line) {
306
+ const text = line.line.trimEnd();
307
+ if (!text)
308
+ return;
309
+ const label = line.stream === "stderr" ? pc.red("stderr") : pc.dim("stdout");
310
+ const color = line.stream === "stderr" ? pc.red : pc.dim;
311
+ process.stdout.write(`${label} ${color(text)}
312
+ `);
313
+ }
294
314
  function printEvent(event, data, state) {
295
315
  switch (event) {
296
316
  case "step_started":
@@ -424,45 +444,81 @@ ${pc.bold(pc.yellow("⊘ Run canceled"))}
424
444
  }
425
445
  }
426
446
  var TERMINAL_EVENTS = new Set(["run_complete", "run_failed", "run_canceled"]);
447
+ var CONSOLE_POLL_MS = 500;
427
448
  async function streamLogs(runId, opts) {
428
449
  const client = makeClient(opts);
450
+ const view = opts.view ?? "all";
429
451
  const state = {
430
452
  agentStart: new Map,
431
453
  agentLabel: new Map,
432
454
  startedAt: Date.now()
433
455
  };
434
456
  let lastSeq = 0;
457
+ let lastLogId = 0;
458
+ let stopConsole = false;
435
459
  const MAX_RETRIES = 5;
436
- for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
437
- let settled = false;
438
- try {
439
- for await (const ev of client.streamRunLogs(runId, { lastEventId: lastSeq })) {
440
- const seq = ev.seq ?? 0;
441
- if (seq > 0)
442
- lastSeq = seq;
443
- printEvent(ev.event, ev, state);
444
- if (TERMINAL_EVENTS.has(ev.event)) {
445
- settled = true;
446
- break;
460
+ const drainConsole = async () => {
461
+ const lines = await client.listRunLogs(runId, { afterId: lastLogId, limit: 200, direction: "asc" });
462
+ for (const line of lines) {
463
+ if (line.id > lastLogId)
464
+ lastLogId = line.id;
465
+ printConsoleLine(line);
466
+ }
467
+ };
468
+ const consoleLoop = view === "agent" ? null : (async () => {
469
+ while (!stopConsole) {
470
+ await drainConsole().catch((err) => {
471
+ if (err instanceof AgentComposeError2 && err.status === 404)
472
+ return;
473
+ throw err;
474
+ });
475
+ if (!stopConsole)
476
+ await new Promise((r) => setTimeout(r, CONSOLE_POLL_MS));
477
+ }
478
+ await drainConsole().catch(() => {});
479
+ })();
480
+ try {
481
+ for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
482
+ let settled = false;
483
+ try {
484
+ for await (const ev of client.streamRunLogs(runId, { lastEventId: lastSeq })) {
485
+ const seq = ev.seq ?? 0;
486
+ if (seq > 0)
487
+ lastSeq = seq;
488
+ if (view !== "console" || TERMINAL_EVENTS.has(ev.event)) {
489
+ printEvent(ev.event, ev, state);
490
+ }
491
+ if (TERMINAL_EVENTS.has(ev.event)) {
492
+ settled = true;
493
+ break;
494
+ }
447
495
  }
496
+ } catch (err) {
497
+ if (err instanceof AgentComposeError2) {
498
+ console.error(`Error: failed to connect to log stream (${err.status})`);
499
+ process.exit(1);
500
+ }
501
+ throw err;
448
502
  }
449
- } catch (err) {
450
- if (err instanceof AgentComposeError2) {
451
- console.error(`Error: failed to connect to log stream (${err.status})`);
452
- process.exit(1);
453
- }
454
- throw err;
503
+ if (settled)
504
+ return;
505
+ if (attempt < MAX_RETRIES)
506
+ await new Promise((r) => setTimeout(r, 200));
455
507
  }
456
- if (settled)
457
- return;
458
- if (attempt < MAX_RETRIES)
459
- await new Promise((r) => setTimeout(r, 200));
508
+ } finally {
509
+ stopConsole = true;
510
+ await consoleLoop;
460
511
  }
461
512
  console.error("Error: stream closed without terminal event after retries");
462
513
  process.exit(1);
463
514
  }
464
- var logsCommand = new Command2("logs").description("Stream logs for a run").argument("<run-id>", "Run ID").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (runId, opts) => {
465
- await streamLogs(runId, opts);
515
+ var logsCommand = new Command2("logs").description("Stream logs for a run").argument("<run-id>", "Run ID").option("--agent", "Show structured agent/lifecycle events only").option("--console", "Show raw runner stdout/stderr only").option("--url <url>", "Server URL", parseUrlFlag, defaultUrl).option("--api-key <key>", "API key", defaultApiKey).action(async (runId, opts) => {
516
+ if (opts.agent && opts.console) {
517
+ console.error("Error: choose only one of --agent or --console");
518
+ process.exit(1);
519
+ }
520
+ const view = opts.agent ? "agent" : opts.console ? "console" : "all";
521
+ await streamLogs(runId, { ...opts, view });
466
522
  });
467
523
 
468
524
  // src/commands/invoke.ts
@@ -1160,11 +1216,38 @@ var listCommand2 = new Command13("list").description("List events for a run, or
1160
1216
  });
1161
1217
  var eventsCommand = new Command13("events").description("Send or list run events (Events API)").addCommand(sendCommand).addCommand(listCommand2);
1162
1218
 
1163
- // src/commands/schedule.ts
1219
+ // src/commands/files.ts
1220
+ import { readFileSync as readFileSync4, writeFileSync } from "node:fs";
1164
1221
  import { Command as Command14 } from "commander";
1165
- 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);
1166
- var scheduleCommand = new Command14("schedule").description("Manage cron schedules attached to registered workflows");
1167
- 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) => {
1168
1251
  const client = makeClient(opts);
1169
1252
  const created = await client.createSchedule({
1170
1253
  name,
@@ -1174,7 +1257,7 @@ sharedOpts2(scheduleCommand.command("create <name>").description("Create a new s
1174
1257
  });
1175
1258
  console.log(`✓ Schedule "${created.name}" → workflow "${created.workflow}" @ "${created.cron}" (${created.id})`);
1176
1259
  });
1177
- 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) => {
1178
1261
  const client = makeClient(opts);
1179
1262
  const rows = await client.listSchedules(opts.factory);
1180
1263
  if (rows.length === 0) {
@@ -1187,21 +1270,21 @@ sharedOpts2(scheduleCommand.command("list").description("List schedules in a fac
1187
1270
  console.log(` ${s.name.padEnd(24)} ${s.workflowName.padEnd(20)} ${s.cron.padEnd(16)} next: ${next} ${s.id}`);
1188
1271
  }
1189
1272
  });
1190
- 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) => {
1191
1274
  const client = makeClient(opts);
1192
1275
  await client.deleteSchedule(id, opts.factory);
1193
1276
  console.log(`✓ Schedule ${id} deleted from factory "${opts.factory}"`);
1194
1277
  });
1195
1278
 
1196
1279
  // src/commands/upgrade.ts
1197
- import { Command as Command15 } from "commander";
1280
+ import { Command as Command16 } from "commander";
1198
1281
  import { spawnSync } from "node:child_process";
1199
- import { readFileSync as readFileSync4 } from "node:fs";
1282
+ import { readFileSync as readFileSync5 } from "node:fs";
1200
1283
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1201
1284
  import pc4 from "picocolors";
1202
1285
  var PACKAGE_NAME = "@agent-compose/cli";
1203
- 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) => {
1204
- 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"));
1205
1288
  const current = pkg.version;
1206
1289
  const controller = new AbortController;
1207
1290
  const timeout = setTimeout(() => controller.abort(), 5000);
@@ -1243,7 +1326,7 @@ var upgradeCommand = new Command15("upgrade").description("Update the agentc CLI
1243
1326
 
1244
1327
  // src/update-check.ts
1245
1328
  import { spawnSync as spawnSync2 } from "node:child_process";
1246
- 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";
1247
1330
  import { homedir as homedir3 } from "node:os";
1248
1331
  import { join as join3 } from "node:path";
1249
1332
  import { createInterface } from "node:readline/promises";
@@ -1283,7 +1366,7 @@ function isNewer(latest, current) {
1283
1366
  }
1284
1367
  function readCache() {
1285
1368
  try {
1286
- const raw = readFileSync5(cachePath(), "utf8");
1369
+ const raw = readFileSync6(cachePath(), "utf8");
1287
1370
  const parsed = JSON.parse(raw);
1288
1371
  if (typeof parsed.checkedAt !== "number")
1289
1372
  return null;
@@ -1299,7 +1382,7 @@ function writeCache(entry) {
1299
1382
  try {
1300
1383
  const path = cachePath();
1301
1384
  mkdirSync2(join3(path, ".."), { recursive: true });
1302
- writeFileSync(path, JSON.stringify(entry, null, 2) + `
1385
+ writeFileSync2(path, JSON.stringify(entry, null, 2) + `
1303
1386
  `, "utf8");
1304
1387
  } catch {}
1305
1388
  }
@@ -1372,7 +1455,7 @@ async function checkForUpdate(currentVersion) {
1372
1455
  }
1373
1456
 
1374
1457
  // src/index.ts
1375
- 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"));
1376
1459
  program.name("agentc").description("Coordinate agent production lines.").version(pkg.version);
1377
1460
  program.hook("preAction", async () => {
1378
1461
  try {
@@ -1386,6 +1469,7 @@ program.addCommand(cancelCommand);
1386
1469
  program.addCommand(scheduleCommand);
1387
1470
  program.addCommand(logsCommand);
1388
1471
  program.addCommand(eventsCommand);
1472
+ program.addCommand(filesCommand);
1389
1473
  program.addCommand(factoryCommand);
1390
1474
  program.addCommand(snapshotCommand);
1391
1475
  program.addCommand(secretsCommand);
@@ -1399,6 +1483,7 @@ Topics:
1399
1483
  Workflows register, invoke, list, cancel
1400
1484
  Scheduling schedule
1401
1485
  Run inspection logs, events
1486
+ Factory drive files
1402
1487
  Resources factory, snapshot, secrets
1403
1488
  Account keys, auth, usage
1404
1489
  Setup init, upgrade
@@ -1408,6 +1493,7 @@ Examples:
1408
1493
  $ agentc invoke pipeline --follow
1409
1494
  $ agentc schedule create nightly --workflow pipeline --cron '0 9 * * *'
1410
1495
  $ agentc logs <run-id>
1496
+ $ agentc files put report.md runs/abc123/report.md
1411
1497
 
1412
1498
  Docs:
1413
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.2",
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": {
@@ -44,7 +44,7 @@
44
44
  "prepublishOnly": "bun run build"
45
45
  },
46
46
  "dependencies": {
47
- "@agent-compose/sdk": "^0.5.1",
47
+ "@agent-compose/sdk": "^0.5.2",
48
48
  "commander": "^12.0.0",
49
49
  "picocolors": "^1.1.1"
50
50
  },
@@ -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,22 +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 `templates/workflow-memory.ts` (or
359
- `.agentc/smoketest/workflows/workflow-memory.ts` for the smoke
360
- variant).
361
- 2. "Run `agentc register ./workflow-memory.ts` once to install it."
362
- 3. "Now any workflow with `memory: true` will trigger it after each
363
- successful run."
364
-
365
- For custom post-run workflows (analytics, audit, notifications)
366
- without the built-in extractor, use `postRunHooks: [...]` instead.
367
- Each post-hook runs in declaration order with the source run's full
368
- context.