@agent-compose/cli 0.1.3 → 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 +62 -23
- package/package.json +3 -3
- package/skills/ac:events.md +98 -0
- package/skills/ac:generate-workflow.md +67 -14
- package/skills/ac:snapshots.md +128 -0
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
@@ -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(
|
|
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,
|
|
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 (
|
|
190
|
-
console.log(`[register] Boots from
|
|
191
|
-
if (
|
|
192
|
-
console.log(`[register] Captures snapshot
|
|
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
|
-
...
|
|
206
|
-
...
|
|
207
|
-
...
|
|
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
|
|
214
|
-
const { id: runId } = await client.invoke(name, {}, {
|
|
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 \`
|
|
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.
|
|
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.
|
|
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
|
|
888
|
-
const
|
|
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
|
|
899
|
-
|
|
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("
|
|
903
|
-
await makeClient(opts).
|
|
904
|
-
|
|
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.
|
|
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": {
|
|
@@ -38,13 +38,13 @@
|
|
|
38
38
|
"typecheck": "tsc --noEmit",
|
|
39
39
|
"test": "bun test src/__tests__",
|
|
40
40
|
"build": "bun run clean && bun run build:js && bun run build:chmod",
|
|
41
|
-
"build:js": "bun build src/index.ts --outdir dist --target node --format esm --packages external --banner='#!/usr/bin/env
|
|
41
|
+
"build:js": "bun build src/index.ts --outdir dist --target node --format esm --packages external --banner='#!/usr/bin/env bun'",
|
|
42
42
|
"build:chmod": "chmod +x dist/index.js",
|
|
43
43
|
"clean": "rm -rf dist",
|
|
44
44
|
"prepublishOnly": "bun run build"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@agent-compose/sdk": "^0.
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
[`
|
|
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
|
-
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
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.
|