@agent-compose/sdk 0.5.7 → 0.5.8
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/agent/__tests__/run-agent-liveness.test.d.ts +17 -0
- package/dist/agent/agent-context.d.ts +67 -0
- package/dist/agent/agent-loop.d.ts +1 -0
- package/dist/client.d.ts +65 -2
- package/dist/index.d.ts +6 -3
- package/dist/index.js +226 -22
- package/dist/pause/wrappers.d.ts +7 -11
- package/dist/runtimes/openai-desktop.js +223 -22
- package/dist/sandbox.d.ts +24 -0
- package/dist/step-invocation/types.d.ts +1 -1
- package/dist/types/execution-context.d.ts +1 -3
- package/dist/types/workflow-metadata.d.ts +12 -1
- package/dist/types/workflow.d.ts +9 -2
- package/dist/utils/bundler.d.ts +3 -1
- package/dist/workflow-steps/workflow.d.ts +4 -1
- package/package.json +1 -1
- package/src/agent/agent-context.ts +212 -0
- package/src/agent/agent-loop.ts +31 -12
- package/src/agent/run-agent.ts +37 -1
- package/src/client.ts +89 -2
- package/src/index.ts +6 -2
- package/src/pause/wrappers.ts +7 -21
- package/src/sandbox.ts +78 -23
- package/src/step-invocation/types.ts +1 -1
- package/src/types/execution-context.ts +1 -3
- package/src/types/workflow-metadata.ts +14 -1
- package/src/types/workflow.ts +9 -3
- package/src/utils/bundler.ts +4 -1
- package/src/workflow-steps/workflow.ts +4 -1
- package/src/workflows/invoke-child.ts +7 -1
package/dist/pause/wrappers.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The two opinionated pause wrappers (ADR-0006 §"SDK surface"), each a thin
|
|
3
3
|
* closure over `ctx.pause`:
|
|
4
4
|
*
|
|
5
|
-
* - `requestDecision` — pause for a typed human/agent decision (schema required).
|
|
6
5
|
* - `sleep` — a lightweight timed pause; resolves on its own TTL,
|
|
7
6
|
* skips the snapshot, returns void.
|
|
8
7
|
* - `waitForEvent` — pause until an event resumes by correlation key.
|
|
9
8
|
*
|
|
9
|
+
* A typed human/agent decision is NOT a wrapper — it's a plain `ctx.pause`
|
|
10
|
+
* with a `schema` (and `payload.options` for the dashboard's answer UI). The
|
|
11
|
+
* pause primitive carries the reason (the ask) and the resume value (the
|
|
12
|
+
* resolution); there is no separate `requestDecision`.
|
|
13
|
+
*
|
|
10
14
|
* Built from a `PauseFn` so the wrapper logic lives in one place and the step
|
|
11
15
|
* runner just spreads them onto the context next to `pause`.
|
|
12
16
|
*/
|
|
@@ -16,16 +20,9 @@ import type { PauseRequest } from "./pause-core.js";
|
|
|
16
20
|
/** The public `ctx.pause` callable (always `kind: "custom"`). */
|
|
17
21
|
export type PauseFn = <T = unknown>(req: PauseRequest<T>) => Promise<T>;
|
|
18
22
|
/** Internal: pause with an explicit wire `kind`. The wrappers stamp
|
|
19
|
-
* `
|
|
23
|
+
* `sleep`/`event` through this; the public `ctx.pause` is always
|
|
20
24
|
* `custom` and never exposes it. */
|
|
21
25
|
export type KindedPauseFn = <T = unknown>(req: PauseRequest<T>, kind: StepPauseRequest["kind"]) => Promise<T>;
|
|
22
|
-
export interface RequestDecisionRequest<T> {
|
|
23
|
-
reason: string;
|
|
24
|
-
payload?: Record<string, unknown>;
|
|
25
|
-
/** Required — a decision is always validated against a shape. */
|
|
26
|
-
schema: z.ZodType<T>;
|
|
27
|
-
ttlMs?: number;
|
|
28
|
-
}
|
|
29
26
|
export interface WaitForEventRequest<T> {
|
|
30
27
|
reason: string;
|
|
31
28
|
/** Required — the by-key resume route targets this. */
|
|
@@ -34,7 +31,6 @@ export interface WaitForEventRequest<T> {
|
|
|
34
31
|
ttlMs?: number;
|
|
35
32
|
}
|
|
36
33
|
export interface PauseWrappers {
|
|
37
|
-
requestDecision<T>(req: RequestDecisionRequest<T>): Promise<T>;
|
|
38
34
|
sleep(durationMs: number): Promise<void>;
|
|
39
35
|
waitForEvent<T = unknown>(req: WaitForEventRequest<T>): Promise<T>;
|
|
40
36
|
}
|
|
@@ -540,6 +540,8 @@ function extractMetadata(source) {
|
|
|
540
540
|
out.outputSchema = freezeMetadataValue(source.outputSchema);
|
|
541
541
|
if (source.snapshots !== undefined)
|
|
542
542
|
out.snapshots = Object.freeze({ ...source.snapshots });
|
|
543
|
+
if (source.resources !== undefined)
|
|
544
|
+
out.resources = Object.freeze({ ...source.resources });
|
|
543
545
|
if (source.processors !== undefined)
|
|
544
546
|
out.processors = Object.freeze([...source.processors]);
|
|
545
547
|
if (source.connectors !== undefined)
|
|
@@ -617,7 +619,6 @@ function compileRunForm(def, metadata) {
|
|
|
617
619
|
agentEvents: stepCtx.agentEvents,
|
|
618
620
|
checkpoint: stepCtx.checkpoint,
|
|
619
621
|
pause: stepCtx.pause,
|
|
620
|
-
requestDecision: stepCtx.requestDecision,
|
|
621
622
|
sleep: stepCtx.sleep,
|
|
622
623
|
waitForEvent: stepCtx.waitForEvent,
|
|
623
624
|
processors: metadata.processors ?? []
|
|
@@ -812,6 +813,7 @@ class AgentComposeClient {
|
|
|
812
813
|
...opts?.snapshots !== undefined ? { snapshots: opts.snapshots } : {},
|
|
813
814
|
...opts?.networkPolicy !== undefined ? { networkPolicy: opts.networkPolicy } : {},
|
|
814
815
|
...opts?.placeholders !== undefined ? { placeholders: opts.placeholders } : {},
|
|
816
|
+
...opts?.size !== undefined ? { size: opts.size } : {},
|
|
815
817
|
...parentRunId ? { parentRunId } : {},
|
|
816
818
|
...opts?.agentId ? { agentId: opts.agentId } : {}
|
|
817
819
|
}
|
|
@@ -971,6 +973,21 @@ class AgentComposeClient {
|
|
|
971
973
|
q2.set("revision", String(opts.revision));
|
|
972
974
|
return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/content?${q2}`, { responseType: "text" });
|
|
973
975
|
}
|
|
976
|
+
async listMembers() {
|
|
977
|
+
const body = await this.fetch("/api/v1/team/members");
|
|
978
|
+
return body.members;
|
|
979
|
+
}
|
|
980
|
+
async createMentions(input) {
|
|
981
|
+
const factorySlug = input.factorySlug ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined) ?? DEFAULT_FACTORY;
|
|
982
|
+
const runToken = typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_RUN_TOKEN : undefined;
|
|
983
|
+
const { factorySlug: _omit, ...payload } = input;
|
|
984
|
+
const body = await this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/mentions`, {
|
|
985
|
+
method: "POST",
|
|
986
|
+
body: payload,
|
|
987
|
+
...runToken ? { headers: { "x-run-token": runToken } } : {}
|
|
988
|
+
});
|
|
989
|
+
return body.mentions;
|
|
990
|
+
}
|
|
974
991
|
async listFactoryEvents(opts) {
|
|
975
992
|
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
976
993
|
const q2 = new URLSearchParams;
|
|
@@ -1222,6 +1239,7 @@ async function bundleWorkflow(workflowPath, overrides) {
|
|
|
1222
1239
|
...inputSchema !== undefined ? { inputSchema } : {},
|
|
1223
1240
|
...outputSchema !== undefined ? { outputSchema } : {},
|
|
1224
1241
|
...metadata.snapshots !== undefined ? { snapshots: metadata.snapshots } : {},
|
|
1242
|
+
...metadata.resources !== undefined ? { resources: metadata.resources } : {},
|
|
1225
1243
|
...metadata.connectors !== undefined ? { connectors: metadata.connectors } : {},
|
|
1226
1244
|
...metadata.connectorOperation !== undefined ? { connectorOperation: metadata.connectorOperation } : {},
|
|
1227
1245
|
...metadata.invokePolicy !== undefined ? { invokePolicy: metadata.invokePolicy } : {}
|
|
@@ -1853,7 +1871,7 @@ async function runControlPoller(opts) {
|
|
|
1853
1871
|
// src/agent/agent-loop.ts
|
|
1854
1872
|
var DEFAULT_CLAUDE_MODEL = "claude-fable-5";
|
|
1855
1873
|
var SAME_BLOCKER_ITERATIONS = 3;
|
|
1856
|
-
var STALL_ITERATIONS =
|
|
1874
|
+
var STALL_ITERATIONS = 6;
|
|
1857
1875
|
var MESSAGE_PREVIEW_CHARS = 400;
|
|
1858
1876
|
function parseAgentStatus(text) {
|
|
1859
1877
|
const match = text.match(/<status>([\s\S]*?)<\/status>/);
|
|
@@ -2084,7 +2102,15 @@ Re-emit the COMPLETE corrected <response> JSON now: every required field present
|
|
|
2084
2102
|
}
|
|
2085
2103
|
const msg = outputVerdict.value;
|
|
2086
2104
|
opts.onAgentEvent?.(iteration, msg);
|
|
2087
|
-
|
|
2105
|
+
const summary = summarizeAgentMessage(msg);
|
|
2106
|
+
opts.onAgentLifecycleEvent?.({
|
|
2107
|
+
event: "agent.message",
|
|
2108
|
+
at: Date.now(),
|
|
2109
|
+
agentId,
|
|
2110
|
+
label,
|
|
2111
|
+
iteration: iteration + 1,
|
|
2112
|
+
message: summary.type === "usage" && client.model != null ? { ...summary, model: client.model } : summary
|
|
2113
|
+
});
|
|
2088
2114
|
if (msg.type === "init")
|
|
2089
2115
|
lastSessionId = msg.sessionId;
|
|
2090
2116
|
if (msg.type === "text")
|
|
@@ -2163,9 +2189,9 @@ raw: ${JSON.stringify(rawResponse).slice(0, 400)}
|
|
|
2163
2189
|
if (!status && rawResponse === null) {
|
|
2164
2190
|
if (++iterationsWithoutStatus >= STALL_ITERATIONS)
|
|
2165
2191
|
throw new Error(`${logLabel} stalled: no <status> block after ${iterationsWithoutStatus} iterations`);
|
|
2166
|
-
if (
|
|
2167
|
-
lastResponseValidationError = "no <response> block
|
|
2168
|
-
process.stdout.write(`${logLabel} no <status
|
|
2192
|
+
if (schemaRetriesLeft-- > 0) {
|
|
2193
|
+
lastResponseValidationError = "Your turn ended with no <status> block" + (opts.responseSchema ? " (and no <response> block)" : "") + ". " + "If you launched a background job (`… &`) and are waiting to be notified when it finishes — STOP: nothing will notify you here. " + "Poll it NOW (read its output / wait for it synchronously to completion), then emit your <status>" + (opts.responseSchema ? " and the complete <response> JSON." : ".");
|
|
2194
|
+
process.stdout.write(`${logLabel} empty turn (no <status>) — corrective re-prompt (${schemaRetriesLeft} retries left)
|
|
2169
2195
|
`);
|
|
2170
2196
|
iteration--;
|
|
2171
2197
|
continue;
|
|
@@ -2746,6 +2772,13 @@ class SandboxUnavailableError extends Error {
|
|
|
2746
2772
|
// src/sandbox.ts
|
|
2747
2773
|
var AGENT_COMPOSE_TAG = process.env.AGENT_COMPOSE_TAG ?? `agent-compose-${process.env.AGENT_COMPOSE_ENV ?? "dev"}`;
|
|
2748
2774
|
var VERCEL_VM_LIFETIME_WINDOW_MS = 6 * 60 * 60 * 1000;
|
|
2775
|
+
var SANDBOX_VCPUS = {
|
|
2776
|
+
"2vcpu-4gb": 2,
|
|
2777
|
+
"4vcpu-8gb": 4,
|
|
2778
|
+
"8vcpu-16gb": 8,
|
|
2779
|
+
"32vcpu-64gb": 32
|
|
2780
|
+
};
|
|
2781
|
+
var DEFAULT_SANDBOX_SIZE = "2vcpu-4gb";
|
|
2749
2782
|
function makeSandboxProvider(sb) {
|
|
2750
2783
|
const commands = sb.commands;
|
|
2751
2784
|
return {
|
|
@@ -2911,7 +2944,7 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
|
|
|
2911
2944
|
let stdout = "";
|
|
2912
2945
|
let stderr = "";
|
|
2913
2946
|
const handle = await sb.runCommand({ cmd: "sh", args: ["-c", cmd], cwd: opts?.cwd, env: mergeEnvs(opts?.envs), detached: true, signal, ...opts?.sudo ? { sudo: true } : {} }).catch((err) => asUnavailable(err, true));
|
|
2914
|
-
|
|
2947
|
+
const collect = async () => {
|
|
2915
2948
|
await pRetry(async (attempt) => {
|
|
2916
2949
|
const h = attempt === 1 ? handle : await sb.getCommand(handle.cmdId);
|
|
2917
2950
|
if (attempt > 1) {
|
|
@@ -2930,11 +2963,22 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
|
|
|
2930
2963
|
}, { retries: 3, minTimeout: 1000, factor: 2 });
|
|
2931
2964
|
const finished = await handle.wait();
|
|
2932
2965
|
return { exitCode: finished.exitCode, stdout, stderr };
|
|
2966
|
+
};
|
|
2967
|
+
let timer;
|
|
2968
|
+
const deadline = opts?.timeoutMs ? new Promise((_, reject) => {
|
|
2969
|
+
timer = setTimeout(() => reject(new Error(`command timed out after ${opts.timeoutMs}ms: ${cmd.slice(0, 200)}`)), opts.timeoutMs);
|
|
2970
|
+
}) : undefined;
|
|
2971
|
+
try {
|
|
2972
|
+
return await (deadline ? Promise.race([collect(), deadline]) : collect());
|
|
2933
2973
|
} catch (err) {
|
|
2934
|
-
if (
|
|
2974
|
+
if (err instanceof Error && err.message.startsWith("command timed out after"))
|
|
2975
|
+
throw err;
|
|
2976
|
+
if (signal?.aborted)
|
|
2935
2977
|
throw new Error(`command timed out after ${opts?.timeoutMs}ms: ${cmd.slice(0, 200)}`);
|
|
2936
|
-
}
|
|
2937
2978
|
return asUnavailable(err, false);
|
|
2979
|
+
} finally {
|
|
2980
|
+
if (timer)
|
|
2981
|
+
clearTimeout(timer);
|
|
2938
2982
|
}
|
|
2939
2983
|
}
|
|
2940
2984
|
},
|
|
@@ -3011,7 +3055,8 @@ var SANDBOX_PROVIDERS = {
|
|
|
3011
3055
|
const np = opts.networkPolicy ? { networkPolicy: toVercelNetworkPolicy(opts.networkPolicy) } : {};
|
|
3012
3056
|
const tags = buildVercelTags(opts.metadata);
|
|
3013
3057
|
const tmpl = opts.template ?? process.env.VERCEL_DEFAULT_SNAPSHOT;
|
|
3014
|
-
const
|
|
3058
|
+
const resources = { vcpus: SANDBOX_VCPUS[opts.size ?? DEFAULT_SANDBOX_SIZE] };
|
|
3059
|
+
const sb = await VercelSandbox.create(tmpl ? { source: { type: "snapshot", snapshotId: tmpl }, timeout: opts.timeoutMs, env: opts.envs, tags, persistent: false, resources, ...np, ...creds } : { runtime: "node24", timeout: opts.timeoutMs, env: opts.envs, tags, persistent: false, resources, ...np, ...creds });
|
|
3015
3060
|
return makeVercelSandboxProvider(sb, opts.envs);
|
|
3016
3061
|
},
|
|
3017
3062
|
reconnect: async (sandboxId) => {
|
|
@@ -3057,7 +3102,7 @@ var SANDBOX_PROVIDERS = {
|
|
|
3057
3102
|
},
|
|
3058
3103
|
e2b: {
|
|
3059
3104
|
requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
|
|
3060
|
-
create: async ({ template, timeoutMs, networkPolicy: _np, ...rest }) => {
|
|
3105
|
+
create: async ({ template, timeoutMs, networkPolicy: _np, size: _size, ...rest }) => {
|
|
3061
3106
|
const maxMs = e2bMaxSandboxMs();
|
|
3062
3107
|
if (timeoutMs > maxMs) {
|
|
3063
3108
|
console.warn(`[sandbox] e2b create timeout clamped: requested ${timeoutMs}ms exceeds the plan cap ${maxMs}ms — ` + `the sandbox dies at the cap, not the requested deadline. Raise E2B_MAX_SANDBOX_MS on plans that allow more.`);
|
|
@@ -3099,7 +3144,7 @@ var SANDBOX_PROVIDERS = {
|
|
|
3099
3144
|
},
|
|
3100
3145
|
"e2b-desktop": {
|
|
3101
3146
|
requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
|
|
3102
|
-
create: async ({ template, timeoutMs, networkPolicy: _np, ...rest }) => {
|
|
3147
|
+
create: async ({ template, timeoutMs, networkPolicy: _np, size: _size, ...rest }) => {
|
|
3103
3148
|
if (!template)
|
|
3104
3149
|
throw new Error("E2B Desktop provider requires an explicit `template` (Dockerfile-based — no default base image)");
|
|
3105
3150
|
return makeDesktopSandboxProvider(await Desktop.create(template, { ...rest, timeoutMs: Math.min(timeoutMs, e2bMaxSandboxMs()) }));
|
|
@@ -3283,14 +3328,6 @@ function scopedCheckpoint(scope) {
|
|
|
3283
3328
|
// src/pause/wrappers.ts
|
|
3284
3329
|
function buildPauseWrappers(pause) {
|
|
3285
3330
|
return {
|
|
3286
|
-
requestDecision(req) {
|
|
3287
|
-
return pause({
|
|
3288
|
-
reason: req.reason,
|
|
3289
|
-
schema: req.schema,
|
|
3290
|
-
...req.payload !== undefined ? { payload: req.payload } : {},
|
|
3291
|
-
...req.ttlMs !== undefined ? { ttlMs: req.ttlMs } : {}
|
|
3292
|
-
}, "decision");
|
|
3293
|
-
},
|
|
3294
3331
|
sleep(durationMs) {
|
|
3295
3332
|
return pause({
|
|
3296
3333
|
reason: `sleep ${durationMs}ms`,
|
|
@@ -3600,7 +3637,7 @@ function buildInvokeChild(runId, opts = {}) {
|
|
|
3600
3637
|
return (name, input, childOpts) => getChildClient().invokeAndWait(name, input, {
|
|
3601
3638
|
...childOpts,
|
|
3602
3639
|
parentRunId: runId,
|
|
3603
|
-
factorySlug: childOpts?.factorySlug ?? opts.defaultFactorySlug ?? process.env.AGENT_COMPOSE_FACTORY_SLUG ?? "default"
|
|
3640
|
+
factorySlug: childOpts?.factorySlug ?? opts.defaultFactorySlug ?? process.env.AGENT_COMPOSE_FACTORY ?? process.env.AGENT_COMPOSE_FACTORY_SLUG ?? "default"
|
|
3604
3641
|
});
|
|
3605
3642
|
}
|
|
3606
3643
|
// src/workflow-steps/step.ts
|
|
@@ -3967,6 +4004,149 @@ async function serveStep(handler) {
|
|
|
3967
4004
|
import { z as z11 } from "zod";
|
|
3968
4005
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3969
4006
|
|
|
4007
|
+
// src/agent/agent-context.ts
|
|
4008
|
+
var AGENT_COMPOSE_MANUAL = `# Working inside an Agent Compose sandbox
|
|
4009
|
+
|
|
4010
|
+
You are an agent running in a per-run sandbox on the Agent Compose platform.
|
|
4011
|
+
Use the **\`agentc\` CLI** and the **\`@agent-compose/sdk\`** for everything below —
|
|
4012
|
+
do NOT hand-roll raw HTTP/curl calls against the platform API. The CLI is on
|
|
4013
|
+
your PATH and already authenticated from the environment
|
|
4014
|
+
(\`AGENT_COMPOSE_URL\` / \`AGENT_COMPOSE_API_KEY\` / \`AGENT_COMPOSE_FACTORY\` are
|
|
4015
|
+
injected for this run), so commands just work — no login, no keys to manage.
|
|
4016
|
+
|
|
4017
|
+
The \`/ac:*\` skills are installed as Claude Code slash commands (\`/ac:invoke\`,
|
|
4018
|
+
\`/ac:events\`, \`/ac:logs\`, \`/ac:register\`, …) — reach for them too.
|
|
4019
|
+
|
|
4020
|
+
## Files — your outputs persist by default
|
|
4021
|
+
|
|
4022
|
+
Your working directory defaults to **\`$AGENT_COMPOSE_RUN_DIR\`** — a per-run
|
|
4023
|
+
directory on the shared factory drive
|
|
4024
|
+
(\`$AGENT_COMPOSE_FACTORY_DIR/<workflow>/<version>/<run-id>/\`) the platform
|
|
4025
|
+
creates and attributes to this run. **Files you write here persist by
|
|
4026
|
+
default** — they show up in the dashboard's Files tab and the run's Artifacts
|
|
4027
|
+
card, with no API calls to save them. The dir already exists and is writable.
|
|
4028
|
+
|
|
4029
|
+
Need throwaway scratch — heavy build output, package caches, temp files?
|
|
4030
|
+
\`cd /tmp\` (or any path outside \`/factory\`): anything off the factory drive is
|
|
4031
|
+
ephemeral and discarded when the sandbox ends. In short: **stay in your working
|
|
4032
|
+
dir to keep something, \`cd\` out to throw it away.**
|
|
4033
|
+
|
|
4034
|
+
The whole shared drive is POSIX-mounted at \`/factory\`; the dashboard-visible
|
|
4035
|
+
root is \`$AGENT_COMPOSE_FACTORY_DIR\` (\`/factory/files\`). Earlier versions and
|
|
4036
|
+
runs live in sibling dirs under
|
|
4037
|
+
\`$AGENT_COMPOSE_FACTORY_DIR/$AGENT_COMPOSE_WORKFLOW/\` — read them for prior
|
|
4038
|
+
context. Other workflows' dirs are present but not your concern.
|
|
4039
|
+
|
|
4040
|
+
## Events — the factory timeline
|
|
4041
|
+
|
|
4042
|
+
Record something on the run/factory timeline (the dashboard renders these)
|
|
4043
|
+
with the CLI — your run id is \`$RUN_ID\`:
|
|
4044
|
+
|
|
4045
|
+
agentc events send "$RUN_ID" <name> --summary "<one line>" [--body '<json>']
|
|
4046
|
+
|
|
4047
|
+
Names like \`note.created\` / \`brief.posted\` surface in the Workbench;
|
|
4048
|
+
\`agentc events list\` reads them back. \`/ac:events\` is the skill equivalent.
|
|
4049
|
+
|
|
4050
|
+
## Runs
|
|
4051
|
+
|
|
4052
|
+
agentc list # registered workflows (/ac:list)
|
|
4053
|
+
agentc logs "$RUN_ID" # a run's logs (/ac:logs)
|
|
4054
|
+
agentc invoke <workflow> -i '<json>' # dispatch a workflow (/ac:invoke)
|
|
4055
|
+
|
|
4056
|
+
## Writing workflow / agent code — the SDK
|
|
4057
|
+
|
|
4058
|
+
\`@agent-compose/sdk\` is installed in \`/workspace\` — import it from any script
|
|
4059
|
+
you write there:
|
|
4060
|
+
|
|
4061
|
+
import { defineWorkflow, agent, AgentComposeClient } from "@agent-compose/sdk";
|
|
4062
|
+
|
|
4063
|
+
Use \`/ac:generate-workflow\` / \`/ac:generate-agent\` to scaffold, then
|
|
4064
|
+
\`agentc register <file.ts>\` (or \`/ac:register\`).
|
|
4065
|
+
|
|
4066
|
+
## Pausing to ask the human — \`agentc pause\`
|
|
4067
|
+
|
|
4068
|
+
When you can't or shouldn't proceed without a human, run \`agentc pause\`. It
|
|
4069
|
+
blocks until they answer on the dashboard, then prints their answer to stdout:
|
|
4070
|
+
|
|
4071
|
+
ANSWER=$(agentc pause --reason "Notion returned 401 — connect Notion to continue" \\
|
|
4072
|
+
--option retry --option skip)
|
|
4073
|
+
|
|
4074
|
+
Reach for it the moment you hit — or foresee — any of these:
|
|
4075
|
+
- **A wall only a human can clear:** a 401/403, a missing credential, an
|
|
4076
|
+
unconnected provider, a host the network refuses. Do NOT retry blindly or try
|
|
4077
|
+
to work around it — pause and say what needs enabling.
|
|
4078
|
+
- **A durable or outward-facing action that needs sign-off:** registering a
|
|
4079
|
+
workflow, deploying, sending email/messages, deleting or overwriting shared
|
|
4080
|
+
data, spending money. Prepare everything, then pause for approval BEFORE you
|
|
4081
|
+
commit it.
|
|
4082
|
+
- **A judgment call only the human can settle:** an under-specified request,
|
|
4083
|
+
several valid paths, a conflict with existing state, missing input only they have.
|
|
4084
|
+
|
|
4085
|
+
You compose the \`--reason\` (the ask) yourself; pass \`--option\` choices when
|
|
4086
|
+
there are clear ones, omit them for a free-form answer. Read the printed answer
|
|
4087
|
+
and act on it. Each agent pauses independently — pausing doesn't stop the others.
|
|
4088
|
+
|
|
4089
|
+
## Credentials
|
|
4090
|
+
|
|
4091
|
+
Connector credentials (Google, GitHub, …) are NEVER in your environment.
|
|
4092
|
+
They're injected at the network layer when you call an allowed host — make the
|
|
4093
|
+
request **without** an Authorization header and the platform adds it. Don't try
|
|
4094
|
+
to read or exfiltrate tokens; they aren't here. The "Connectors & access"
|
|
4095
|
+
section below (when present) lists exactly which providers this run can reach.
|
|
4096
|
+
|
|
4097
|
+
## Tools in this environment
|
|
4098
|
+
|
|
4099
|
+
- \`agentc\` — Agent Compose CLI (your primary interface; authed from env)
|
|
4100
|
+
- \`@agent-compose/sdk\` — installed in /workspace for writing workflows
|
|
4101
|
+
- \`/ac:*\` Claude Code skills — slash commands for the above
|
|
4102
|
+
- \`archil\` (factory drive), \`rtk\`, \`bun\`
|
|
4103
|
+
- A world-writable \`/workspace\` working directory`;
|
|
4104
|
+
function renderConnectorsSection(connectors) {
|
|
4105
|
+
if (connectors.length === 0)
|
|
4106
|
+
return "";
|
|
4107
|
+
const rows = connectors.map((c) => {
|
|
4108
|
+
const host = c.hosts?.length ? c.hosts.join(", ") : "(host set by the platform)";
|
|
4109
|
+
const verbs = c.methods?.length ? c.methods.join("/") : "any method";
|
|
4110
|
+
const paths = c.pathPrefixes?.length ? ` under ${c.pathPrefixes.join(", ")}` : "";
|
|
4111
|
+
const repo = c.repository ? ` — repo \`${c.repository}\` (${c.access ?? "read"})` : "";
|
|
4112
|
+
const why = c.scopes?.length ? `
|
|
4113
|
+
_scopes: ${c.scopes.join(", ")}_` : "";
|
|
4114
|
+
return `- **${c.name ?? c.provider}** → \`${host}\` — ${verbs}${paths}${repo}${why}`;
|
|
4115
|
+
});
|
|
4116
|
+
return `
|
|
4117
|
+
|
|
4118
|
+
## Connectors & access — what this run can reach
|
|
4119
|
+
|
|
4120
|
+
These providers are connected for this run. Call their APIs with plain
|
|
4121
|
+
fetch/SDKs and **no Authorization header** — the platform injects the
|
|
4122
|
+
credential at the network layer. Requests outside the listed method/path are
|
|
4123
|
+
refused (403) and the token withheld. Anything NOT listed is unreachable; if
|
|
4124
|
+
you need it, \`agentc pause\` and ask for it to be connected.
|
|
4125
|
+
|
|
4126
|
+
${rows.join(`
|
|
4127
|
+
`)}
|
|
4128
|
+
`;
|
|
4129
|
+
}
|
|
4130
|
+
function buildAgentContextDoc(env) {
|
|
4131
|
+
let connectors = [];
|
|
4132
|
+
const raw = env.AGENT_COMPOSE_CONNECTORS;
|
|
4133
|
+
if (raw) {
|
|
4134
|
+
try {
|
|
4135
|
+
const parsed = JSON.parse(raw);
|
|
4136
|
+
if (Array.isArray(parsed))
|
|
4137
|
+
connectors = parsed;
|
|
4138
|
+
} catch {}
|
|
4139
|
+
}
|
|
4140
|
+
return AGENT_COMPOSE_MANUAL + renderConnectorsSection(connectors);
|
|
4141
|
+
}
|
|
4142
|
+
async function writeAgentContext(args) {
|
|
4143
|
+
const doc = buildAgentContextDoc(args.env);
|
|
4144
|
+
const dir = args.cwd.replace(/\/+$/, "") || "/workspace";
|
|
4145
|
+
for (const name of ["AGENTS.md", "CLAUDE.md", "GEMINI.md"]) {
|
|
4146
|
+
await args.sandbox.files.write(`${dir}/${name}`, doc);
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
|
|
3970
4150
|
// src/agent/protocol-suffix.md
|
|
3971
4151
|
var protocol_suffix_default = '## Status Signal\n\nWhen you have finished your work or are blocked, emit a `<status>` block at the end of your response:\n\n```json\n<status>\n{\n "summary": "one sentence describing what was done or what is blocking",\n "completed": ["each acceptance criterion that is now fully met"],\n "blockers": [],\n "changed_files": ["relative/path/to/file"],\n "tests_run": true,\n "exit_signal": true\n}\n</status>\n```\n\n**Field semantics:**\n- `summary`: one sentence — what was accomplished or what is blocking\n- `completed`: acceptance criteria items that are fully done — be specific\n- `blockers`: non-empty when `exit_signal: false` — describe the exact obstacle\n- `changed_files`: relative paths of files you created or modified\n- `tests_run`: `true` if you ran any test suite (pass or fail); `false` if no tests exist or you skipped them\n- `exit_signal: true` — set when ALL acceptance criteria are met and no blockers remain\n- `exit_signal: false` — set when blocked or unfinished; `blockers` must be non-empty\n\n**If you are still actively working** and have not reached a natural stopping point, do NOT emit a `<status>` block — just keep working.\n\n**Example (done):**\n\n```json\n<status>\n{\n "summary": "Added input validation middleware to /api/tasks with tests",\n "completed": ["POST /api/tasks validates required fields", "Returns 400 with details on invalid input", "Unit tests passing"],\n "blockers": [],\n "changed_files": ["src/middleware/validate.ts", "src/routes/tasks.ts", "tests/validate.test.ts"],\n "tests_run": true,\n "exit_signal": true\n}\n</status>\n```\n\n**Example (blocked):**\n\n```json\n<status>\n{\n "summary": "Implemented middleware but tests are failing due to module resolution",\n "completed": ["Middleware created and wired into route"],\n "blockers": ["Tests fail: cannot resolve import \'./validate\' — module resolution config unclear"],\n "changed_files": ["src/middleware/validate.ts"],\n "tests_run": true,\n "exit_signal": false\n}\n</status>\n```\n';
|
|
3972
4152
|
|
|
@@ -4082,7 +4262,28 @@ function resolveAgentId(explicitId) {
|
|
|
4082
4262
|
return `step${activeCall.stepIndex}-agent-${activeCall.callIndex}`;
|
|
4083
4263
|
}
|
|
4084
4264
|
async function agent(opts) {
|
|
4085
|
-
|
|
4265
|
+
let workingDir = opts.workingDir || process.env.AGENT_COMPOSE_RUN_DIR || "/workspace";
|
|
4266
|
+
const CONTEXT_WRITE_DEADLINE_MS = 15000;
|
|
4267
|
+
const tryWriteContext = (cwd) => {
|
|
4268
|
+
let timer;
|
|
4269
|
+
return Promise.race([
|
|
4270
|
+
writeAgentContext({ sandbox: opts.sandbox, cwd, env: process.env }).then(() => true),
|
|
4271
|
+
new Promise((resolve) => {
|
|
4272
|
+
timer = setTimeout(() => resolve(false), CONTEXT_WRITE_DEADLINE_MS);
|
|
4273
|
+
})
|
|
4274
|
+
]).catch((err) => {
|
|
4275
|
+
console.error(`[agent] writeAgentContext(${cwd}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4276
|
+
return false;
|
|
4277
|
+
}).finally(() => {
|
|
4278
|
+
if (timer)
|
|
4279
|
+
clearTimeout(timer);
|
|
4280
|
+
});
|
|
4281
|
+
};
|
|
4282
|
+
if (!await tryWriteContext(workingDir) && workingDir !== "/workspace") {
|
|
4283
|
+
console.error(`[agent] working dir ${workingDir} not writable within ${CONTEXT_WRITE_DEADLINE_MS}ms ` + `(factory drive degraded?) — falling back to /workspace so the agent can run`);
|
|
4284
|
+
workingDir = "/workspace";
|
|
4285
|
+
await tryWriteContext(workingDir);
|
|
4286
|
+
}
|
|
4086
4287
|
const agentId = resolveAgentId(opts.agentId);
|
|
4087
4288
|
const callbackUrl = process.env.AGENT_COMPOSE_URL;
|
|
4088
4289
|
const callbackToken = process.env.AGENT_COMPOSE_RUN_TOKEN;
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -39,6 +39,27 @@ export type SandboxNetworkPolicy = "allow-all" | "deny-all" | {
|
|
|
39
39
|
allow?: string[] | Record<string, SandboxNetworkAllowRule[]>;
|
|
40
40
|
subnets?: SandboxNetworkSubnetPolicy;
|
|
41
41
|
};
|
|
42
|
+
/** Sandbox machine size. A coarse small/medium/large knob that maps to
|
|
43
|
+
* provider machine specs at create time. Vercel honours it natively via
|
|
44
|
+
* `resources.vcpus` (2048 MB RAM per vCPU). E2B sizing is baked into the
|
|
45
|
+
* template, so E2B ignores this field. Default: "small". */
|
|
46
|
+
/** Sandbox hardware SKU. Named for the actual machine spec (vCPU + RAM) rather
|
|
47
|
+
* than abstract t-shirt sizes. Memory is always 2048 MB per vCPU:
|
|
48
|
+
* 2vcpu-4gb = 2 vCPU / 4 GiB (Vercel's own default machine)
|
|
49
|
+
* 4vcpu-8gb = 4 vCPU / 8 GiB
|
|
50
|
+
* 8vcpu-16gb = 8 vCPU / 16 GiB (per-sandbox ceiling on STANDARD accounts —
|
|
51
|
+
* probed live: 16 & 32 vCPU 400 on dev)
|
|
52
|
+
* 32vcpu-64gb = 32 vCPU / 64 GiB (ENTERPRISE ONLY — standard accounts reject >8 vCPU) */
|
|
53
|
+
export type SandboxSize = "2vcpu-4gb" | "4vcpu-8gb" | "8vcpu-16gb" | "32vcpu-64gb";
|
|
54
|
+
/** SKU → Vercel vCPU count (RAM follows at 2048 MB/vCPU). */
|
|
55
|
+
export declare const SANDBOX_VCPUS: Record<SandboxSize, number>;
|
|
56
|
+
/** SDK fallback size when neither the caller nor the deployment specifies one.
|
|
57
|
+
* Deliberately conservative — the OPERATIONAL default is chosen per-environment
|
|
58
|
+
* by the server via the `SANDBOX_DEFAULT_SIZE` env var (prod = Enterprise →
|
|
59
|
+
* `32vcpu-64gb`; dev = `8vcpu-16gb`, since the dev account caps at 8 vCPU).
|
|
60
|
+
* TODO(sandbox-size): the prod default is temporarily `32vcpu-64gb` for a
|
|
61
|
+
* memory-hungry workload — lower it back when no longer needed. */
|
|
62
|
+
export declare const DEFAULT_SANDBOX_SIZE: SandboxSize;
|
|
42
63
|
export interface SandboxCreateOpts {
|
|
43
64
|
envs: Record<string, string>;
|
|
44
65
|
metadata: Record<string, string>;
|
|
@@ -46,6 +67,9 @@ export interface SandboxCreateOpts {
|
|
|
46
67
|
/** Provider-specific template/snapshot identifier. E2B: template id or
|
|
47
68
|
* snapshot id (omit → E2B's default base). Vercel: snapshot id (omit → node24). */
|
|
48
69
|
template?: string;
|
|
70
|
+
/** Machine size. Vercel maps it to `resources.vcpus`; E2B ignores it
|
|
71
|
+
* (size is template-defined). Omit → `DEFAULT_SANDBOX_SIZE`. */
|
|
72
|
+
size?: SandboxSize;
|
|
49
73
|
/** Outbound request policy with header transforms — ONE shape for every
|
|
50
74
|
* provider; only the enforcement point differs. Vercel's firewall
|
|
51
75
|
* enforces + injects natively from this value. E2B enforces it via an
|
|
@@ -48,7 +48,7 @@ export type StepResult<TOutput = unknown> = {
|
|
|
48
48
|
* it so the runtime type and the parsed shape can't drift.
|
|
49
49
|
*
|
|
50
50
|
* Loose by design (the inner zod shape stops at orchestration fields
|
|
51
|
-
* the engine needs): the SDK wrapper layer (`
|
|
51
|
+
* the engine needs): the SDK wrapper layer (`sleep` /
|
|
52
52
|
* `sleep` / `waitForEvent`) owns its own payload contract, and the
|
|
53
53
|
* engine treats `payload` as opaque. */
|
|
54
54
|
export declare const StepPauseRequestSchema: z.ZodObject<{
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { InvokeAndWaitOptions, RunStatus } from "../client.js";
|
|
3
3
|
import type { RequestContext } from "../request-context/request-context.js";
|
|
4
4
|
import type { PauseRequest } from "../pause/pause-core.js";
|
|
5
|
-
import type {
|
|
5
|
+
import type { WaitForEventRequest } from "../pause/wrappers.js";
|
|
6
6
|
import type { SandboxProvider } from "./sandbox.js";
|
|
7
7
|
/** The identity of this workflow run. */
|
|
8
8
|
export interface WorkflowRun {
|
|
@@ -38,8 +38,6 @@ export interface BaseExecutionContext {
|
|
|
38
38
|
* PauseExpiredError / PauseSchemaError. See ADR-0006 / ADR-0011.
|
|
39
39
|
*/
|
|
40
40
|
pause<T = unknown>(req: PauseRequest<T>): Promise<T>;
|
|
41
|
-
/** Pause for a typed decision (a `schema` is required). Wrapper over `pause`. */
|
|
42
|
-
requestDecision<T>(req: RequestDecisionRequest<T>): Promise<T>;
|
|
43
41
|
/** Lightweight timed pause — resolves after `durationMs`, no snapshot. */
|
|
44
42
|
sleep(durationMs: number): Promise<void>;
|
|
45
43
|
/** Pause until an event resumes by `correlationKey`. Wrapper over `pause`. */
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Keep this module free of value imports from `types/workflow.ts` or
|
|
9
9
|
* `workflow-steps/workflow.ts` — it is the cycle-break point.
|
|
10
10
|
*/
|
|
11
|
-
import type { SandboxNetworkPolicy } from "../sandbox.js";
|
|
11
|
+
import type { SandboxNetworkPolicy, SandboxSize } from "../sandbox.js";
|
|
12
12
|
import type { Processor } from "../processors/processor.js";
|
|
13
13
|
/**
|
|
14
14
|
* Workflow-level metadata read by the server at registration. Lives on
|
|
@@ -134,6 +134,15 @@ export interface InvokePolicy {
|
|
|
134
134
|
apiKeys?: string[];
|
|
135
135
|
workflows?: string[];
|
|
136
136
|
}
|
|
137
|
+
/** Sandbox machine resources for a workflow's runs. A coarse size knob
|
|
138
|
+
* today; kept as its own object so finer controls (disk, gpu, …) can be
|
|
139
|
+
* added later without reshaping `WorkflowMetadata`. */
|
|
140
|
+
export interface SandboxResources {
|
|
141
|
+
/** Machine size — `small | medium | large`. Maps to provider specs at
|
|
142
|
+
* create time (Vercel: 2 / 4 / 8 vCPU, 2048 MB RAM per vCPU). Omit →
|
|
143
|
+
* `"small"`. E2B sizing is template-defined and ignores this. */
|
|
144
|
+
size?: SandboxSize;
|
|
145
|
+
}
|
|
137
146
|
export interface WorkflowMetadata {
|
|
138
147
|
/** One-line, human-readable description of what the workflow does.
|
|
139
148
|
* Surfaced on the dashboard template tile + run page header. Authors
|
|
@@ -154,6 +163,8 @@ export interface WorkflowMetadata {
|
|
|
154
163
|
outputSchema?: IOSchema;
|
|
155
164
|
/** All snapshot config — boot source + capture mode. */
|
|
156
165
|
snapshots?: SnapshotConfig;
|
|
166
|
+
/** Sandbox machine resources (size). Optional; omit → small. */
|
|
167
|
+
resources?: SandboxResources;
|
|
157
168
|
processors?: readonly Processor[];
|
|
158
169
|
/** Connector requirements — providers whose APIs this workflow calls.
|
|
159
170
|
* Dispatch resolves an authorized grant per provider and injects a
|
package/dist/types/workflow.d.ts
CHANGED
|
@@ -27,8 +27,8 @@ export type { WorkflowMetadata } from "./workflow-metadata.js";
|
|
|
27
27
|
export interface AgentEventSink {
|
|
28
28
|
emit(event: AgentLifecycleEvent): void | Promise<void>;
|
|
29
29
|
}
|
|
30
|
-
import type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema } from "./workflow-metadata.js";
|
|
31
|
-
export type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema };
|
|
30
|
+
import type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema, SandboxResources } from "./workflow-metadata.js";
|
|
31
|
+
export type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema, SandboxResources };
|
|
32
32
|
/** Turn/iteration budget for `agent(opts)`. Re-exported here so authors
|
|
33
33
|
* can type per-invoke budget overrides they pass as workflow input. */
|
|
34
34
|
export interface AgentBudget {
|
|
@@ -100,6 +100,13 @@ export interface WorkflowDefinition<TOutput = unknown, TInput extends Record<str
|
|
|
100
100
|
* `invoke({ snapshots })` overrides this default.
|
|
101
101
|
*/
|
|
102
102
|
snapshots?: SnapshotConfig;
|
|
103
|
+
/**
|
|
104
|
+
* Sandbox machine size — `small` (default) | `medium` | `large`. Maps to
|
|
105
|
+
* provider machine specs at create (Vercel: 2 / 4 / 8 vCPU, 2048 MB RAM
|
|
106
|
+
* per vCPU). Optional; omit for `small`. E2B sizing is template-defined
|
|
107
|
+
* and ignores this. Per-invocation `invoke({ size })` overrides it.
|
|
108
|
+
*/
|
|
109
|
+
resources?: SandboxResources;
|
|
103
110
|
/**
|
|
104
111
|
* Outbound network policy for the runner sandbox.
|
|
105
112
|
* Use "*": [] to allow all traffic while still injecting headers for specific domains.
|
package/dist/utils/bundler.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* manifest's `sourceHash` against the source it received.
|
|
21
21
|
*/
|
|
22
22
|
import type { SnapshotConfig } from "../types/workflow.js";
|
|
23
|
-
import type { IOSchema, ConnectorRequirements, ConnectorOperationTag, InvokePolicy } from "../types/workflow-metadata.js";
|
|
23
|
+
import type { IOSchema, ConnectorRequirements, ConnectorOperationTag, InvokePolicy, SandboxResources } from "../types/workflow-metadata.js";
|
|
24
24
|
import type { SandboxNetworkPolicy } from "../sandbox.js";
|
|
25
25
|
import { type WorkflowPlan } from "../types/workflow-plan.js";
|
|
26
26
|
/** Bumped when the manifest contract changes in a way the server should
|
|
@@ -84,6 +84,8 @@ export interface BundledWorkflow {
|
|
|
84
84
|
/** Snapshot config from the workflow definition — `bootFrom` (where to
|
|
85
85
|
* restore at run start), `save`, `retain`. */
|
|
86
86
|
snapshots?: SnapshotConfig;
|
|
87
|
+
/** Sandbox machine size declared via `defineWorkflow({ resources: { size } })`. */
|
|
88
|
+
resources?: SandboxResources;
|
|
87
89
|
workflowPlan: WorkflowPlan;
|
|
88
90
|
/** Compact JSON-Schema-shaped description of the workflow's input
|
|
89
91
|
* type. Extracted from the workflow's declared `input` zod schema
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import type { z } from "zod";
|
|
22
22
|
import type { Step, Workflow } from "./types.js";
|
|
23
|
-
import type { SnapshotConfig } from "../types/workflow-metadata.js";
|
|
23
|
+
import type { SnapshotConfig, SandboxResources } from "../types/workflow-metadata.js";
|
|
24
24
|
import type { SandboxNetworkPolicy } from "../sandbox.js";
|
|
25
25
|
import type { Processor } from "../processors/processor.js";
|
|
26
26
|
export interface WorkflowBuilder<TInput, TCurrent> {
|
|
@@ -46,6 +46,9 @@ export interface StepWorkflowDefinition<TInput, TOutput> {
|
|
|
46
46
|
networkPolicy?: SandboxNetworkPolicy;
|
|
47
47
|
placeholders?: Record<string, string>;
|
|
48
48
|
snapshots?: SnapshotConfig;
|
|
49
|
+
/** Sandbox machine size — small (default) | medium | large. Vercel maps
|
|
50
|
+
* it to vCPUs; E2B sizing is template-defined. Omit → small. */
|
|
51
|
+
resources?: SandboxResources;
|
|
49
52
|
processors?: readonly Processor[];
|
|
50
53
|
}
|
|
51
54
|
export declare function createStepWorkflow<TInput, TOutput>(opts: StepWorkflowDefinition<TInput, TOutput>): WorkflowBuilder<TInput, TInput>;
|
package/package.json
CHANGED