@agent-compose/cli 0.2.0 → 0.2.2
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 +139 -8
- package/package.json +1 -1
- package/skills/ac:demo.md +7 -3
- package/skills/ac:generate-workflow.md +343 -122
- package/skills/ac:snapshots.md +10 -2
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
|
|
6
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
7
7
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
8
8
|
import { program } from "commander";
|
|
9
9
|
|
|
@@ -227,7 +227,7 @@ var registerCommand = new Command("register").description("Register a workflow w
|
|
|
227
227
|
while (Date.now() < deadline) {
|
|
228
228
|
const { status } = await client.getStatus(runId);
|
|
229
229
|
if (status === "success") {
|
|
230
|
-
console.log(`✓ Snapshot ready
|
|
230
|
+
console.log(`✓ Snapshot ready. Run \`agentc snapshot list --factory ${opts.factory ?? "default"} --workflow ${name}\` to find the snapshot id, then reference it as \`snapshots: { bootFrom: { snapshotId: "<id>" } }\` on another workflow.`);
|
|
231
231
|
return;
|
|
232
232
|
}
|
|
233
233
|
if (status === "failed" || status === "abandoned") {
|
|
@@ -240,11 +240,7 @@ var registerCommand = new Command("register").description("Register a workflow w
|
|
|
240
240
|
process.exit(1);
|
|
241
241
|
});
|
|
242
242
|
function formatBootFrom(b) {
|
|
243
|
-
|
|
244
|
-
return `snapshot ${b.snapshotId}`;
|
|
245
|
-
if (b.workflow)
|
|
246
|
-
return b.version ? `${b.workflow}@${b.version}` : `${b.workflow} (latest)`;
|
|
247
|
-
return "(unknown)";
|
|
243
|
+
return `snapshot ${b.snapshotId}`;
|
|
248
244
|
}
|
|
249
245
|
|
|
250
246
|
// src/commands/invoke.ts
|
|
@@ -1163,9 +1159,144 @@ var listCommand2 = new Command13("list").description("List events for a run, or
|
|
|
1163
1159
|
});
|
|
1164
1160
|
var eventsCommand = new Command13("events").description("Send or list run events (Events API)").addCommand(sendCommand).addCommand(listCommand2);
|
|
1165
1161
|
|
|
1162
|
+
// src/update-check.ts
|
|
1163
|
+
import { spawnSync } from "node:child_process";
|
|
1164
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
1165
|
+
import { homedir as homedir3 } from "node:os";
|
|
1166
|
+
import { join as join3 } from "node:path";
|
|
1167
|
+
import { createInterface } from "node:readline/promises";
|
|
1168
|
+
import pc4 from "picocolors";
|
|
1169
|
+
var PACKAGE_NAME = "@agent-compose/cli";
|
|
1170
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
1171
|
+
var FETCH_TIMEOUT_MS = 2000;
|
|
1172
|
+
function cachePath() {
|
|
1173
|
+
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
1174
|
+
return join3(xdg && xdg.length > 0 ? xdg : join3(homedir3(), ".config"), "agentc", "update-check.json");
|
|
1175
|
+
}
|
|
1176
|
+
function shouldSkip() {
|
|
1177
|
+
if (process.env.AGENTC_NO_UPDATE_CHECK === "1")
|
|
1178
|
+
return true;
|
|
1179
|
+
if (process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true" || process.env.BUILDKITE === "true" || process.env.CIRCLECI === "true" || process.env.GITLAB_CI === "true" || process.env.JENKINS_URL !== undefined)
|
|
1180
|
+
return true;
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
function isNewer(latest, current) {
|
|
1184
|
+
const parse = (v) => {
|
|
1185
|
+
const m = v.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
1186
|
+
if (!m)
|
|
1187
|
+
return null;
|
|
1188
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
1189
|
+
};
|
|
1190
|
+
const a = parse(latest);
|
|
1191
|
+
const b = parse(current);
|
|
1192
|
+
if (!a || !b)
|
|
1193
|
+
return false;
|
|
1194
|
+
for (let i = 0;i < 3; i++) {
|
|
1195
|
+
if (a[i] > b[i])
|
|
1196
|
+
return true;
|
|
1197
|
+
if (a[i] < b[i])
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
function readCache() {
|
|
1203
|
+
try {
|
|
1204
|
+
const raw = readFileSync4(cachePath(), "utf8");
|
|
1205
|
+
const parsed = JSON.parse(raw);
|
|
1206
|
+
if (typeof parsed.checkedAt !== "number")
|
|
1207
|
+
return null;
|
|
1208
|
+
return {
|
|
1209
|
+
checkedAt: parsed.checkedAt,
|
|
1210
|
+
latestVersion: typeof parsed.latestVersion === "string" ? parsed.latestVersion : null
|
|
1211
|
+
};
|
|
1212
|
+
} catch {
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
function writeCache(entry) {
|
|
1217
|
+
try {
|
|
1218
|
+
const path = cachePath();
|
|
1219
|
+
mkdirSync2(join3(path, ".."), { recursive: true });
|
|
1220
|
+
writeFileSync(path, JSON.stringify(entry, null, 2) + `
|
|
1221
|
+
`, "utf8");
|
|
1222
|
+
} catch {}
|
|
1223
|
+
}
|
|
1224
|
+
async function fetchLatest() {
|
|
1225
|
+
const controller = new AbortController;
|
|
1226
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
1227
|
+
try {
|
|
1228
|
+
const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
1229
|
+
signal: controller.signal,
|
|
1230
|
+
headers: { Accept: "application/json" }
|
|
1231
|
+
});
|
|
1232
|
+
if (!res.ok)
|
|
1233
|
+
return null;
|
|
1234
|
+
const body = await res.json();
|
|
1235
|
+
return typeof body.version === "string" ? body.version : null;
|
|
1236
|
+
} catch {
|
|
1237
|
+
return null;
|
|
1238
|
+
} finally {
|
|
1239
|
+
clearTimeout(timeout);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
async function promptYesNo(question) {
|
|
1243
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1244
|
+
try {
|
|
1245
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
1246
|
+
return answer === "y" || answer === "yes";
|
|
1247
|
+
} finally {
|
|
1248
|
+
rl.close();
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
async function resolveLatest() {
|
|
1252
|
+
const cached = readCache();
|
|
1253
|
+
if (cached && Date.now() - cached.checkedAt < CHECK_INTERVAL_MS) {
|
|
1254
|
+
return cached.latestVersion;
|
|
1255
|
+
}
|
|
1256
|
+
const latest = await fetchLatest();
|
|
1257
|
+
writeCache({ checkedAt: Date.now(), latestVersion: latest });
|
|
1258
|
+
return latest;
|
|
1259
|
+
}
|
|
1260
|
+
async function checkForUpdate(currentVersion) {
|
|
1261
|
+
if (shouldSkip())
|
|
1262
|
+
return;
|
|
1263
|
+
const latest = await resolveLatest();
|
|
1264
|
+
if (!latest || !isNewer(latest, currentVersion))
|
|
1265
|
+
return;
|
|
1266
|
+
const headline = `${pc4.yellow("●")} agentc ${pc4.bold(latest)} is available (you have ${currentVersion}).`;
|
|
1267
|
+
process.stderr.write(`${headline}
|
|
1268
|
+
`);
|
|
1269
|
+
if (!process.stdin.isTTY) {
|
|
1270
|
+
process.stderr.write(` Run ${pc4.cyan("npm install -g " + PACKAGE_NAME + "@latest")} to update.
|
|
1271
|
+
`);
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const confirm = await promptYesNo(` Install now? [y/${pc4.bold("N")}] `);
|
|
1275
|
+
if (!confirm)
|
|
1276
|
+
return;
|
|
1277
|
+
process.stderr.write(` Running ${pc4.cyan("npm install -g " + PACKAGE_NAME + "@latest")} …
|
|
1278
|
+
`);
|
|
1279
|
+
const result = spawnSync("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], {
|
|
1280
|
+
stdio: "inherit"
|
|
1281
|
+
});
|
|
1282
|
+
if (result.status !== 0) {
|
|
1283
|
+
process.stderr.write(`${pc4.red("✗")} Install failed. Run the command above manually.
|
|
1284
|
+
`);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
process.stderr.write(`${pc4.green("✓")} Installed ${pc4.bold(`agentc ${latest}`)}. ` + `Re-run your command to use the new version.
|
|
1288
|
+
`);
|
|
1289
|
+
process.exit(0);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1166
1292
|
// src/index.ts
|
|
1167
|
-
var pkg = JSON.parse(
|
|
1293
|
+
var pkg = JSON.parse(readFileSync5(fileURLToPath2(new URL("../package.json", import.meta.url)), "utf8"));
|
|
1168
1294
|
program.name("agentc").description("CLI for agent-compose — register, invoke, and monitor workflows").version(pkg.version);
|
|
1295
|
+
program.hook("preAction", async () => {
|
|
1296
|
+
try {
|
|
1297
|
+
await checkForUpdate(pkg.version);
|
|
1298
|
+
} catch {}
|
|
1299
|
+
});
|
|
1169
1300
|
program.addCommand(registerCommand);
|
|
1170
1301
|
program.addCommand(invokeCommand);
|
|
1171
1302
|
program.addCommand(listCommand);
|
package/package.json
CHANGED
package/skills/ac:demo.md
CHANGED
|
@@ -30,9 +30,13 @@ Make sure the user understands the three primitives before you start
|
|
|
30
30
|
typing. Keep it brief — they can read [`docs/how-it-works.md`](../../docs/how-it-works.md)
|
|
31
31
|
for the full version.
|
|
32
32
|
|
|
33
|
-
> **Workflow** —
|
|
34
|
-
>
|
|
35
|
-
>
|
|
33
|
+
> **Workflow** — the unit of work you author. Two shapes are valid:
|
|
34
|
+
> a single `async (ctx, sandbox) => T` body (run-form, agent-driven)
|
|
35
|
+
> or a typed pipeline of `defineStep(...)` objects composed with
|
|
36
|
+
> `defineWorkflow({...}).step(s1).step(s2).build()` (step-form). The
|
|
37
|
+
> demo below uses run-form because the body is a single agent loop;
|
|
38
|
+
> step-form is the right shape when the work decomposes into phases
|
|
39
|
+
> with typed handoffs.
|
|
36
40
|
|
|
37
41
|
> **Agent loop** — `agent({ runtime, prompt, ... })`. Embedded inside
|
|
38
42
|
> the workflow body. Drives one iteration cycle of an LLM agent against
|
|
@@ -7,131 +7,352 @@ effort: high
|
|
|
7
7
|
|
|
8
8
|
# Generate Workflow
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
10
|
+
Walk the user through building a new workflow, **in their own words**.
|
|
11
|
+
This skill is used by operators and product folks as well as engineers
|
|
12
|
+
— ask questions in plain language and decide the shape internally.
|
|
13
|
+
|
|
14
|
+
## What to ask the user
|
|
15
|
+
|
|
16
|
+
Ask these one at a time. Keep the language non-technical. Don't ask
|
|
17
|
+
about "step-form vs run-form" — that's an internal call.
|
|
18
|
+
|
|
19
|
+
1. **What should this workflow do?**
|
|
20
|
+
One paragraph in plain English. This becomes the workflow's
|
|
21
|
+
`description` (shown on the dashboard tile and run page header).
|
|
22
|
+
Example: "Pulls open pull requests from a GitHub repo, scores each
|
|
23
|
+
one by review urgency, and posts the top five to Slack."
|
|
24
|
+
|
|
25
|
+
2. **What information does it take in?**
|
|
26
|
+
Walk through each input field: a short name, a type (text, number,
|
|
27
|
+
yes/no, list, object), and a one-line description of what it's for.
|
|
28
|
+
Each field's description becomes a `.describe(...)` on the zod
|
|
29
|
+
schema so it shows up in the dashboard's Input panel.
|
|
30
|
+
|
|
31
|
+
3. **What does it produce?**
|
|
32
|
+
Same pattern for the output. Fields + types + per-field
|
|
33
|
+
descriptions. The dashboard's Output panel renders these straight
|
|
34
|
+
from the schema.
|
|
35
|
+
|
|
36
|
+
4. **Where should it live?**
|
|
37
|
+
Project-relative path, e.g. `src/workflows/`. Also ask for a
|
|
38
|
+
kebab-case name if one isn't obvious from the description.
|
|
39
|
+
|
|
40
|
+
5. **Does it need to call external services?**
|
|
41
|
+
If yes, ask which (GitHub, Slack, OpenAI, an internal API, etc.).
|
|
42
|
+
You'll generate a network policy + brokered-secret stubs.
|
|
43
|
+
|
|
44
|
+
6. **Should it run on a schedule?**
|
|
45
|
+
If yes, ask for the cadence in plain English ("every weekday at 9am
|
|
46
|
+
ET", "every hour", "the 1st of every month"). Convert to cron
|
|
47
|
+
yourself.
|
|
48
|
+
|
|
49
|
+
7. **Anything else worth recording?**
|
|
50
|
+
Optional. Most workflows are fine without this.
|
|
51
|
+
|
|
52
|
+
**Do NOT ask** the user about:
|
|
53
|
+
|
|
54
|
+
- step-form vs run-form
|
|
55
|
+
- sandbox environments / dependency installation
|
|
56
|
+
- snapshots: `saveLatest`, `retainSteps`, `bootFrom`
|
|
57
|
+
- `memory` / `postRunHooks` / `processors`
|
|
58
|
+
|
|
59
|
+
Decide those internally based on what they described (see the
|
|
60
|
+
"Internal decisions" section below).
|
|
61
|
+
|
|
62
|
+
## Internal decisions
|
|
63
|
+
|
|
64
|
+
After answering the questions above, decide the shape WITHOUT asking:
|
|
65
|
+
|
|
66
|
+
### Pick the workflow shape
|
|
67
|
+
|
|
68
|
+
- **Step-form** (`defineWorkflow({...}).step(s1).step(s2).build()` with
|
|
69
|
+
`defineStep(...)` objects) — pick this when the body decomposes
|
|
70
|
+
cleanly into named phases with typed handoffs. Each step's output
|
|
71
|
+
threads into the next step's input. The dashboard renders each step
|
|
72
|
+
as a typed phase with its own duration. Examples: ETL pipelines,
|
|
73
|
+
classification → enrichment → publish, fetch → score → rank.
|
|
74
|
+
|
|
75
|
+
- **Run-form** (one `async (ctx, sandbox)` body with `agent({...})`
|
|
76
|
+
calls inside) — pick this when the body is dominated by one or more
|
|
77
|
+
agent loops. Decomposing an LLM iteration into typed engine steps is
|
|
78
|
+
the wrong shape. Use `ctx.step("phase-name", () => …)` inside the
|
|
79
|
+
body for observability sub-events when there's pre-agent setup
|
|
80
|
+
worth tracing on the run timeline.
|
|
81
|
+
|
|
82
|
+
When the user's description says "agent", "LLM", "the model decides",
|
|
83
|
+
"reasons through", "writes a summary based on", "navigates the
|
|
84
|
+
website" — that's run-form. When they describe deterministic phases
|
|
85
|
+
that each transform structured data — that's step-form. If genuinely
|
|
86
|
+
mixed, default to run-form and use `ctx.step` for the deterministic
|
|
87
|
+
phases.
|
|
88
|
+
|
|
89
|
+
### Should it have a sandbox environment?
|
|
90
|
+
|
|
91
|
+
If the workflow needs specific tools, CLIs, or installed packages in
|
|
92
|
+
its runner VM (Playwright + Chromium, a specific Node version, an SDK
|
|
93
|
+
that needs `npm install`-ing first), it needs a **sandbox
|
|
94
|
+
environment** to capture that pre-installed state. The user doesn't
|
|
95
|
+
need to know about this — you decide.
|
|
96
|
+
|
|
97
|
+
When you decide a sandbox env is needed:
|
|
98
|
+
|
|
99
|
+
1. Generate a small `defineSandboxEnvironment` workflow alongside the
|
|
100
|
+
user's workflow. Name it after the deps it captures (e.g.
|
|
101
|
+
`playwright-env`).
|
|
102
|
+
2. Register that first with `agentc register --build` so the snapshot
|
|
103
|
+
is captured.
|
|
104
|
+
3. Reference the captured snapshot from the user's workflow via
|
|
105
|
+
`snapshots: { bootFrom: { snapshotId: "<id from the env's first
|
|
106
|
+
run>" } }`.
|
|
107
|
+
4. Tell the user about it in user terms: "I'll also create a one-time
|
|
108
|
+
setup workflow `playwright-env` that installs Playwright. We
|
|
109
|
+
register that first to capture a baseline image, then your workflow
|
|
110
|
+
boots from that image so it doesn't reinstall on every run."
|
|
111
|
+
|
|
112
|
+
Most workflows DO NOT need this. Default to no sandbox env.
|
|
113
|
+
|
|
114
|
+
### Always include in the generated file
|
|
115
|
+
|
|
116
|
+
- `description: "..."` on the `defineWorkflow` / `defineSandboxEnvironment`
|
|
117
|
+
call — straight from the user's question 1 answer.
|
|
118
|
+
- `.describe("...")` on **every** zod schema field — from the user's
|
|
119
|
+
question 2 + 3 answers. Schema descriptions appear in the dashboard
|
|
120
|
+
IO panels; without them, fields show only their type.
|
|
121
|
+
- `.describe("...")` on the root schema too where helpful — surfaces
|
|
122
|
+
at the panel header.
|
|
123
|
+
|
|
124
|
+
## Templates
|
|
125
|
+
|
|
126
|
+
Generate the appropriate template based on your internal shape
|
|
127
|
+
decision.
|
|
128
|
+
|
|
129
|
+
### Template A — step-form (typed pipeline)
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { defineWorkflow, defineStep } from "@agent-compose/sdk";
|
|
133
|
+
import { z } from "zod";
|
|
134
|
+
|
|
135
|
+
const InputSchema = z.object({
|
|
136
|
+
repo: z.string().describe("GitHub repository as `owner/name`"),
|
|
137
|
+
limit: z.number().int().positive().describe("Maximum number of PRs to score"),
|
|
138
|
+
}).describe("Inputs for the PR triage workflow");
|
|
139
|
+
|
|
140
|
+
const FetchOutput = z.object({
|
|
141
|
+
prs: z.array(z.object({
|
|
142
|
+
number: z.number().describe("PR number"),
|
|
143
|
+
title: z.string().describe("PR title"),
|
|
144
|
+
author: z.string().describe("PR author's GitHub login"),
|
|
145
|
+
})).describe("Open PRs pulled from GitHub"),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const ScoreOutput = z.object({
|
|
149
|
+
scored: z.array(z.object({
|
|
150
|
+
number: z.number().describe("PR number"),
|
|
151
|
+
score: z.number().describe("Review-urgency score, 0..1"),
|
|
152
|
+
})).describe("PRs scored by review urgency"),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const OutputSchema = z.object({
|
|
156
|
+
top: z.array(z.object({
|
|
157
|
+
number: z.number().describe("PR number"),
|
|
158
|
+
score: z.number().describe("Review-urgency score, 0..1"),
|
|
159
|
+
})).describe("The top-scoring PRs, descending by score"),
|
|
160
|
+
}).describe("Top PRs surfaced by the triage workflow");
|
|
161
|
+
|
|
162
|
+
const fetchStep = defineStep({
|
|
163
|
+
name: "fetch-prs",
|
|
164
|
+
input: InputSchema,
|
|
165
|
+
output: FetchOutput,
|
|
166
|
+
run: async ({ input }) => {
|
|
167
|
+
// Pure server-side work: fetch, parse, transform.
|
|
168
|
+
return { prs: [/* ... */] };
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const scoreStep = defineStep({
|
|
173
|
+
name: "score-prs",
|
|
174
|
+
input: FetchOutput,
|
|
175
|
+
output: ScoreOutput,
|
|
176
|
+
run: ({ input }) => ({
|
|
177
|
+
scored: input.prs.map(p => ({ number: p.number, score: 0 })),
|
|
178
|
+
}),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const pickTopStep = defineStep({
|
|
182
|
+
name: "pick-top",
|
|
183
|
+
input: ScoreOutput,
|
|
184
|
+
output: OutputSchema,
|
|
185
|
+
run: ({ input }) => ({ top: input.scored.slice(0, 5) }),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
export default defineWorkflow({
|
|
189
|
+
id: "pr-triage",
|
|
190
|
+
description: "Pulls open PRs from a GitHub repo, scores each one by review urgency, surfaces the top five.",
|
|
191
|
+
input: InputSchema,
|
|
192
|
+
output: OutputSchema,
|
|
193
|
+
})
|
|
194
|
+
.step(fetchStep)
|
|
195
|
+
.step(scoreStep)
|
|
196
|
+
.step(pickTopStep)
|
|
197
|
+
.build();
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Notes:
|
|
201
|
+
- Each step's `output` schema must satisfy the next step's `input`
|
|
202
|
+
schema (the SDK enforces this at `defineWorkflow().step(...)` time
|
|
203
|
+
via TS inference). Reshape inside the upstream step's `run` body,
|
|
204
|
+
not at the boundary.
|
|
205
|
+
- Step bodies don't receive a `sandbox`. If a step needs to run code
|
|
206
|
+
inside the runner VM, that's the agent-driven shape — use run-form.
|
|
207
|
+
|
|
208
|
+
### Template B — run-form (agent-driven body)
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { defineWorkflow, agent, claudeRuntime } from "@agent-compose/sdk";
|
|
212
|
+
import { z } from "zod";
|
|
213
|
+
import PROMPT from "./prompt.md" with { type: "text" };
|
|
214
|
+
|
|
215
|
+
const InputSchema = z.object({
|
|
216
|
+
repo: z.string().describe("GitHub repository as `owner/name`"),
|
|
217
|
+
}).describe("Inputs for the code review agent");
|
|
218
|
+
|
|
219
|
+
const OutputSchema = z.object({
|
|
220
|
+
ok: z.boolean().describe("True when the agent finished its review"),
|
|
221
|
+
summary: z.string().optional().describe("One-paragraph summary of the agent's findings"),
|
|
222
|
+
}).describe("Result of the code review agent");
|
|
223
|
+
|
|
224
|
+
export default defineWorkflow({
|
|
225
|
+
description: "Runs an LLM agent that reviews recent changes in a repo and writes a summary.",
|
|
226
|
+
input: InputSchema,
|
|
227
|
+
output: OutputSchema,
|
|
228
|
+
|
|
229
|
+
async run(ctx, sandbox) {
|
|
230
|
+
// Pre-agent setup. `ctx.step("phase", () => ...)` is for the run
|
|
231
|
+
// timeline — emits step_started / step_completed events. It's
|
|
232
|
+
// observability sugar, not an engine step.
|
|
233
|
+
const data = await ctx.step("fetch-input", async () => {
|
|
234
|
+
// pure-server fetch / preparation
|
|
235
|
+
return { /* ... */ };
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Agent loop — the LLM iterates against the sandbox until it
|
|
239
|
+
// emits `exit_signal: true` or hits the budget.
|
|
240
|
+
const result = await agent({
|
|
241
|
+
sandbox,
|
|
242
|
+
runtime: claudeRuntime,
|
|
243
|
+
prompt: `${PROMPT}\n\nContext: ${JSON.stringify(data)}`,
|
|
244
|
+
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"],
|
|
245
|
+
budget: { turnsPerIteration: 40, maxIterations: 6 },
|
|
246
|
+
// responseSchema: MyZodSchema, // for typed handoffs
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await ctx.setMetadata({ summary: result.status?.summary });
|
|
250
|
+
return {
|
|
251
|
+
ok: !!result.status?.completed,
|
|
252
|
+
summary: result.status?.summary,
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
// ── Optional metadata, attached by the bundler at registration ──
|
|
257
|
+
//
|
|
258
|
+
// networkPolicy: outbound traffic + brokered secret substitution.
|
|
259
|
+
// networkPolicy: {
|
|
260
|
+
// allow: {
|
|
261
|
+
// "api.github.com": [{ transform: [{ headers: {
|
|
262
|
+
// Authorization: "basic:$GITHUB_TOKEN",
|
|
263
|
+
// } }] }],
|
|
264
|
+
// },
|
|
265
|
+
// },
|
|
266
|
+
// // Placeholder values the runner sees in env vars AFTER brokering.
|
|
267
|
+
// // Only needed when a tool / SDK validates the env var format on
|
|
268
|
+
// // startup. The real secret never enters the VM — Vercel's
|
|
269
|
+
// // firewall substitutes it on the way out.
|
|
270
|
+
// placeholders: { GITHUB_TOKEN: "ghp_" + "x".repeat(36) },
|
|
271
|
+
//
|
|
272
|
+
// snapshots — boot source + capture mode:
|
|
273
|
+
// snapshots: {
|
|
274
|
+
// bootFrom: { snapshotId: "snap_abc…" },
|
|
275
|
+
// saveLatest: true,
|
|
276
|
+
// retainSteps: false,
|
|
277
|
+
// },
|
|
278
|
+
//
|
|
279
|
+
// memory — opt-in. Requires `workflow-memory` to be registered in
|
|
280
|
+
// this factory.
|
|
281
|
+
// memory: true,
|
|
282
|
+
//
|
|
283
|
+
// postRunHooks — workflows that run after this one completes:
|
|
284
|
+
// postRunHooks: ["audit-trail", "notify-slack"],
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Notes:
|
|
289
|
+
- Always declare the return type (or let TS infer + show it on hover).
|
|
290
|
+
- `Promise.all` to fan out independent `agent` calls.
|
|
291
|
+
- `ctx.step("phase-name", () => …)` for any named non-agent phase
|
|
292
|
+
worth showing on the dashboard timeline.
|
|
293
|
+
|
|
294
|
+
### Sandbox environment companion (only when you decided one is needed)
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
// playwright-env.ts — captures a baseline VM with Playwright pre-installed.
|
|
298
|
+
// Register this first with `agentc register playwright-env.ts --build`
|
|
299
|
+
// so the snapshot exists before user workflows reference it.
|
|
300
|
+
import { defineSandboxEnvironment } from "@agent-compose/sdk";
|
|
301
|
+
|
|
302
|
+
export default defineSandboxEnvironment({
|
|
303
|
+
name: "playwright-env",
|
|
304
|
+
description: "Sandbox with Playwright + Chromium installed for browser-using agents.",
|
|
305
|
+
setup: async (sb) => {
|
|
306
|
+
await sb.commands.run("sudo npm install -g playwright");
|
|
307
|
+
await sb.commands.run("sudo npx playwright install --with-deps chromium");
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
After `agentc register --build` succeeds, the CLI prints a snapshot
|
|
313
|
+
id; reference it on the user workflow:
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
export default defineWorkflow({
|
|
317
|
+
description: "...",
|
|
318
|
+
input: InputSchema,
|
|
319
|
+
output: OutputSchema,
|
|
320
|
+
snapshots: { bootFrom: { snapshotId: "<id from --build>" } },
|
|
321
|
+
// ...
|
|
322
|
+
});
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Final steps
|
|
326
|
+
|
|
327
|
+
1. Show the user the file(s) you plan to write and confirm before
|
|
328
|
+
writing. In plain language: "I'll create `pr-triage.ts` in
|
|
329
|
+
`src/workflows/`. It takes `{ repo, limit }` and returns
|
|
330
|
+
`{ top: [...] }`. Sound right?"
|
|
331
|
+
2. Write the files.
|
|
332
|
+
3. Suggest `bun run typecheck` (or the project's equivalent) to
|
|
333
|
+
validate.
|
|
334
|
+
4. Tell the user the next commands in plain English:
|
|
335
|
+
- "To register: `agentc register src/workflows/pr-triage.ts`."
|
|
336
|
+
- "To test it: `agentc invoke pr-triage --follow`."
|
|
337
|
+
- (If you added a sandbox env companion: "First register the
|
|
338
|
+
`playwright-env` workflow with `--build` so the snapshot is
|
|
339
|
+
captured, then register and invoke `pr-triage`.")
|
|
123
340
|
|
|
124
341
|
## When the user asks for memory extraction
|
|
125
342
|
|
|
126
|
-
The built-in memory extractor (`workflow-memory`) is a separate
|
|
127
|
-
that must be registered in the same factory. Walk them
|
|
343
|
+
The built-in memory extractor (`workflow-memory`) is a separate
|
|
344
|
+
workflow that must be registered in the same factory. Walk them
|
|
345
|
+
through it in user terms:
|
|
128
346
|
|
|
129
|
-
1.
|
|
347
|
+
1. "I'll add the `workflow-memory` recipe to your project."
|
|
348
|
+
Copy from `templates/workflow-memory.ts` (or
|
|
130
349
|
`.agentc/smoketest/workflows/workflow-memory.ts` for the smoke
|
|
131
|
-
variant)
|
|
132
|
-
2. `agentc register ./workflow-memory.ts` to install it.
|
|
133
|
-
3.
|
|
350
|
+
variant).
|
|
351
|
+
2. "Run `agentc register ./workflow-memory.ts` once to install it."
|
|
352
|
+
3. "Now any workflow with `memory: true` will trigger it after each
|
|
353
|
+
successful run."
|
|
134
354
|
|
|
135
|
-
|
|
136
|
-
use `postRunHooks: [...]` instead.
|
|
137
|
-
with the source run's full
|
|
355
|
+
For custom post-run workflows (analytics, audit, notifications)
|
|
356
|
+
without the built-in extractor, use `postRunHooks: [...]` instead.
|
|
357
|
+
Each post-hook runs in declaration order with the source run's full
|
|
358
|
+
context.
|
package/skills/ac:snapshots.md
CHANGED
|
@@ -107,8 +107,16 @@ export default defineWorkflow({
|
|
|
107
107
|
// On every successful run — captures the sandbox VM after the last step:
|
|
108
108
|
defineWorkflow({ snapshots: { saveLatest: true }, run: ... });
|
|
109
109
|
|
|
110
|
-
// Retain one per step (storage scales with step count)
|
|
111
|
-
|
|
110
|
+
// Retain one snapshot per step (storage scales linearly with step count).
|
|
111
|
+
// Most useful with step-form workflows where each `.step(definedStep)`
|
|
112
|
+
// is a distinct engine step; a run-form workflow has only one outer
|
|
113
|
+
// "run" step, so `retainSteps: true` there behaves the same as
|
|
114
|
+
// `saveLatest: true` alone.
|
|
115
|
+
defineWorkflow({ id: "pipeline", input, output, snapshots: { saveLatest: true, retainSteps: true } })
|
|
116
|
+
.step(fetchStep)
|
|
117
|
+
.step(scoreStep)
|
|
118
|
+
.step(pickTopStep)
|
|
119
|
+
.build();
|
|
112
120
|
```
|
|
113
121
|
|
|
114
122
|
Per-invocation override:
|