@agent-compose/sdk 0.2.5 → 0.3.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.
@@ -1,16 +1,14 @@
1
1
  /**
2
2
  * A sandbox environment is a workflow whose job is to leave its VM in a
3
3
  * configured state, then snapshot it so other workflows can boot from that
4
- * state. Capture is **opt-in**: the wrapper below sets `saveSnapshot: true`
5
- * by default (opt out with `saveSnapshot: false` if you only want side
6
- * effects).
4
+ * state. Capture is **opt-in**: the wrapper below sets
5
+ * `snapshots: { saveLatest: true }` by default (opt out with
6
+ * `saveLatest: false` if you only want side effects).
7
7
  *
8
- * Once captured, reference the snapshot from another workflow's `snapshot`
9
- * field by run UUID, workflow name, or `name@version`:
8
+ * Once captured, reference the snapshot from another workflow's
9
+ * `snapshots.bootFrom` field by provider snapshot id:
10
10
  *
11
- * snapshot: "my-setup" // most recent successful snapshot
12
- * snapshot: "my-setup@v1" // version-scoped recency
13
- * snapshot: "<run-uuid>" // pinned to an exact run
11
+ * snapshots: { bootFrom: { snapshotId: "snap_..." } }
14
12
  *
15
13
  * `defineSandboxEnvironment` is sugar over `defineWorkflow` — it makes the
16
14
  * setup recipe read like an imperative script by supplying the local
@@ -20,7 +18,7 @@
20
18
  * 1. Author a setup file with `defineSandboxEnvironment`.
21
19
  * 2. `agentc register setup.ts --build` — registers and invokes once to
22
20
  * capture the snapshot.
23
- * 3. Other workflows declare `snapshot: "name"` and boot from it.
21
+ * 3. Other workflows declare `snapshots: { bootFrom: { snapshotId } }`.
24
22
  * 4. `agentc snapshot list` / `delete` to manage the Vercel storage bill.
25
23
  *
26
24
  * @example
@@ -39,20 +37,31 @@
39
37
  import type { SandboxProvider } from "./sandbox.js";
40
38
  import { defineWorkflow } from "./workflow.js";
41
39
  import type { Workflow } from "../workflow-steps/types.js";
40
+ import type { SnapshotConfig } from "./workflow-metadata.js";
42
41
 
43
42
  export interface SandboxEnvironmentDefinition {
44
43
  name: string;
45
44
  description?: string;
46
45
  setup: (sb: SandboxProvider) => Promise<void>;
47
- /** Override the sugar's `saveSnapshot: true` default. Set `false` to opt
48
- * out of snapshot capture (rarely useful — an env with no snapshot can't
49
- * be referenced as the `snapshot` field on another workflow). */
50
- saveSnapshot?: boolean;
46
+ /** Override the sugar's `{ saveLatest: true }` default. Set
47
+ * `{ saveLatest: false }` to opt out of snapshot capture (rarely
48
+ * useful — an env with no snapshot can't be referenced as a
49
+ * `bootFrom` on another workflow). */
50
+ snapshots?: SnapshotConfig;
51
+ /** Override the sugar's `memory: false` default. Setup workflows
52
+ * don't typically benefit from memory extraction; opt in explicitly
53
+ * when they do. */
54
+ memory?: boolean;
51
55
  }
52
56
 
53
57
  /** Sugar over `defineWorkflow` for setup-only workflows that exist to
54
58
  * capture a snapshot. The workflow takes no meaningful input and returns
55
- * nothing — its value is the side effect on the sandbox VM. */
59
+ * nothing — its value is the side effect on the sandbox VM.
60
+ *
61
+ * Defaults `memory: false` because sandbox environments emit setup
62
+ * output (npm installs, command exit codes) rather than agent traces
63
+ * worth memorising. Authors can opt in explicitly via `env.memory:
64
+ * true` if their environment somehow does want extraction. */
56
65
  export function defineSandboxEnvironment(
57
66
  env: SandboxEnvironmentDefinition,
58
67
  ): Workflow<Record<string, unknown>, void> {
@@ -60,7 +69,8 @@ export function defineSandboxEnvironment(
60
69
  throw new Error(`defineSandboxEnvironment(${env.name}): 'setup' must be a function`);
61
70
  }
62
71
  return defineWorkflow<void, Record<string, unknown>>({
63
- saveSnapshot: env.saveSnapshot ?? true,
72
+ snapshots: env.snapshots ?? { saveLatest: true },
73
+ memory: env.memory ?? false,
64
74
  run: async (_ctx, sandbox) => env.setup(sandbox),
65
75
  });
66
76
  }
@@ -32,8 +32,12 @@ export interface SandboxProvider {
32
32
  /** Capture the running sandbox's state as a reusable snapshot. Vercel
33
33
  * supports it natively; E2B's model is Dockerfile-based and doesn't map
34
34
  * cleanly — `undefined` on providers that don't. Used by the server's
35
- * `--build` flow to stamp the snapshot id on the workflow row. */
36
- snapshot?(): Promise<{ snapshotId: string }>;
35
+ * `--build` flow to stamp the snapshot id on the workflow row.
36
+ *
37
+ * `sizeBytes` is the on-disk footprint reported by the provider. May
38
+ * be omitted when the provider doesn't expose it; the server stores
39
+ * `null` for missing values rather than estimating. */
40
+ snapshot?(): Promise<{ snapshotId: string; sizeBytes?: number }>;
37
41
  }
38
42
 
39
43
  /** Stateless provider-level snapshot deletion — no live sandbox needed,
@@ -20,18 +20,83 @@ import type { Processor } from "../processors/processor.js";
20
20
  * The bundler reads these from the default export at registration time
21
21
  * and forwards them to the server's POST /api/v1/templates payload.
22
22
  */
23
- /** Server-side knob for whether/how a Workflow Memory agent should run
24
- * after this workflow completes. `"default"` runs the built-in extractor;
25
- * `false` disables; an object names a custom memory workflow to dispatch. */
26
- export type WorkflowMemoryConfig = "default" | false | { workflow: string };
23
+ /** Whether the built-in Workflow Memory extractor should run after this
24
+ * workflow completes. Boolean toggle — custom post-run workflows live
25
+ * in the separate `postRunHooks` array on `WorkflowMetadata`. */
26
+ export type WorkflowMemoryConfig = boolean;
27
+
28
+ /** Where a run boots from. The snapshot id is the unit of identity —
29
+ * each captured snapshot already records the workflow + version it
30
+ * came from on the snapshot row, so there's no separate "latest of
31
+ * workflow X" resolution at dispatch time. Operators pick a snapshot
32
+ * from the dashboard snapshot list (or `agentc snapshot list`) and
33
+ * paste the id here.
34
+ *
35
+ * Omit `bootFrom` entirely to boot a fresh base sandbox. */
36
+ export type BootSnapshot = { snapshotId: string };
37
+
38
+ /** Snapshot configuration — boot source plus capture knobs. One object
39
+ * per workflow / per invocation; collapsing boot + capture under a
40
+ * single key reads as "all snapshot config lives here." */
41
+ export interface SnapshotConfig {
42
+ /** Where the runner restores from at run start. Structured (workflow
43
+ * ref or snapshot id) so the intent is explicit at the call site. */
44
+ bootFrom?: BootSnapshot;
45
+ /** Capture the sandbox state on terminal success. The latest pointer
46
+ * on `workflow_runs.vercel_snapshot_id` always tracks the most
47
+ * recent capture; without `retainSteps`, prior captures are deleted
48
+ * as new ones land — constant storage cost. */
49
+ saveLatest?: boolean;
50
+ /** Only meaningful with `saveLatest: true`. Retain every step's
51
+ * snapshot in `run_step_snapshots` so a future dispatch can boot
52
+ * from a specific step's checkpoint via its `snapshotId`. Cost
53
+ * scales linearly with step count. */
54
+ retainSteps?: boolean;
55
+ }
56
+
57
+ /** JSON-Schema-shaped description captured by the bundler from a
58
+ * workflow's `input` or `output` zod schema. Carried in template
59
+ * metadata so the dashboard can render typed input forms + output
60
+ * type tables. */
61
+ export interface IOSchema {
62
+ /** JSON Schema `type` keyword, when single-valued. Omitted for unions / unknowns. */
63
+ type?: string | string[];
64
+ /** Free-form description pulled from `.describe(...)` on the root. */
65
+ description?: string;
66
+ /** Object-shape: one entry per property. */
67
+ properties?: Record<string, { type?: string | string[]; description?: string }>;
68
+ /** Required property names — relevant only when `type === "object"`. */
69
+ required?: string[];
70
+ }
71
+
72
+ /** Alias kept for backwards source-compatibility with the original
73
+ * output-only release. New code should prefer `IOSchema`. */
74
+ export type OutputSchema = IOSchema;
27
75
 
28
76
  export interface WorkflowMetadata {
29
77
  networkPolicy?: SandboxNetworkPolicy;
30
78
  placeholders?: Record<string, string>;
31
- snapshot?: string;
32
- saveSnapshot?: boolean;
79
+ /** Input schema captured at bundle time from the workflow's
80
+ * declared `input` zod schema. Step-form workflows populate this
81
+ * automatically; run-form workflows (no explicit input schema —
82
+ * defaults to `z.unknown()`) leave it undefined. */
83
+ inputSchema?: IOSchema;
84
+ /** Output schema captured at bundle time from the workflow's
85
+ * declared `output` zod schema. Step-form workflows populate this
86
+ * automatically; run-form workflows whose `run()` returns
87
+ * arbitrarily-typed values leave it undefined. */
88
+ outputSchema?: IOSchema;
89
+ /** All snapshot config — boot source + capture mode. */
90
+ snapshots?: SnapshotConfig;
33
91
  processors?: readonly Processor[];
34
- memory?: WorkflowMemoryConfig;
92
+ /** Run the built-in memory extractor after this workflow completes.
93
+ * Opt-in; defaults to false when omitted. */
94
+ memory?: boolean;
95
+ /** Ordered list of workflow names that run after this workflow
96
+ * completes. The runtime dispatches them in declaration order; the
97
+ * built-in memory extractor (when `memory: true`) runs as a separate
98
+ * hook alongside whatever's declared here. */
99
+ postRunHooks?: readonly string[];
35
100
  }
36
101
 
37
102
  /**
@@ -54,12 +119,12 @@ export function extractMetadata(source: Partial<WorkflowMetadata>): WorkflowMeta
54
119
  const out: WorkflowMetadata = {};
55
120
  if (source.networkPolicy !== undefined) out.networkPolicy = freezeMetadataValue(source.networkPolicy);
56
121
  if (source.placeholders !== undefined) out.placeholders = Object.freeze({ ...source.placeholders });
57
- if (source.snapshot !== undefined) out.snapshot = source.snapshot;
58
- if (source.saveSnapshot !== undefined) out.saveSnapshot = source.saveSnapshot;
122
+ if (source.inputSchema !== undefined) out.inputSchema = freezeMetadataValue(source.inputSchema);
123
+ if (source.outputSchema !== undefined) out.outputSchema = freezeMetadataValue(source.outputSchema);
124
+ if (source.snapshots !== undefined) out.snapshots = Object.freeze({ ...source.snapshots });
59
125
  if (source.processors !== undefined) out.processors = Object.freeze([...source.processors]);
60
- if (source.memory !== undefined) out.memory = typeof source.memory === "object" && source.memory !== null
61
- ? Object.freeze({ ...source.memory })
62
- : source.memory;
126
+ if (source.memory !== undefined) out.memory = source.memory;
127
+ if (source.postRunHooks !== undefined) out.postRunHooks = Object.freeze([...source.postRunHooks]);
63
128
  return Object.freeze(out);
64
129
  }
65
130
 
@@ -37,8 +37,8 @@ export interface AgentEventSink {
37
37
  emit(event: AgentLifecycleEvent): void | Promise<void>;
38
38
  }
39
39
 
40
- import type { WorkflowMemoryConfig } from "./workflow-metadata.js";
41
- export type { WorkflowMemoryConfig };
40
+ import type { WorkflowMemoryConfig, SnapshotConfig, BootSnapshot, IOSchema, OutputSchema } from "./workflow-metadata.js";
41
+ export type { WorkflowMemoryConfig, SnapshotConfig, BootSnapshot, IOSchema, OutputSchema };
42
42
 
43
43
  /** Turn/iteration budget for `agent(opts)`. Re-exported here so authors
44
44
  * can type per-invoke budget overrides they pass as workflow input. */
@@ -91,21 +91,21 @@ export interface WorkflowDefinition<
91
91
  > {
92
92
  run: WorkflowFn<TOutput, TInput>;
93
93
  /**
94
- * Reference to a snapshot the runner should boot from at run start.
95
- * Accepts a run UUID, a workflow name, or `name@version` — any workflow
96
- * registered with `--build` produces a snapshot you can name here. The
97
- * runner VM starts in that pre-configured state. Per-invocation
98
- * `invoke({ snapshot })` overrides this default.
99
- */
100
- snapshot?: string;
101
- /**
102
- * Capture a snapshot of the sandbox on successful /complete. Snapshots
103
- * are long-lived (never auto-expire) — customers list + delete them
104
- * explicitly via `agentc snapshot list/delete`. Per-invocation
105
- * `invoke({ saveSnapshot })` overrides this default.
106
- * `defineSandboxEnvironment` sugar sets this to true by default.
94
+ * All snapshot config — boot source plus capture mode.
95
+ *
96
+ * `snapshots.bootFrom`: the runner restores from this exact provider
97
+ * snapshot id at run start. Omit to boot a fresh sandbox.
98
+ *
99
+ * `snapshots.saveLatest`: `true` captures one snapshot after each
100
+ * successful step (latest-only — prior is freed).
101
+ * `{ saveLatest: true, retainSteps: true }` keeps every step's
102
+ * snapshot for fork / replay / time-travel.
103
+ *
104
+ * Snapshots are long-lived (never auto-expire). List + delete via
105
+ * `agentc snapshot list/delete`. Per-invocation
106
+ * `invoke({ snapshots })` overrides this default.
107
107
  */
108
- saveSnapshot?: boolean;
108
+ snapshots?: SnapshotConfig;
109
109
  /**
110
110
  * Outbound network policy for the runner sandbox.
111
111
  * Use "*": [] to allow all traffic while still injecting headers for specific domains.
@@ -142,10 +142,14 @@ export interface WorkflowDefinition<
142
142
  * }
143
143
  */
144
144
  placeholders?: Record<string, string>;
145
- /** Workflow Memory extraction. Defaults to "default" when omitted.
146
- * Set false to skip memory extraction for this workflow, or point at a
147
- * custom memory workflow once custom extractors are supported. */
145
+ /** Run the built-in memory extractor after this workflow completes.
146
+ * Opt-in; defaults to false when omitted. */
148
147
  memory?: WorkflowMemoryConfig;
148
+ /** Ordered list of workflow names to dispatch as post-hooks after
149
+ * this workflow completes. Each hook receives the source run's
150
+ * context. The memory extractor (when `memory: true`) runs as an
151
+ * additional hook alongside these. */
152
+ postRunHooks?: readonly string[];
149
153
  }
150
154
 
151
155
  /**
@@ -24,7 +24,7 @@ import { createHash } from "node:crypto";
24
24
  import { parse as babelParse } from "@babel/parser";
25
25
  import type { File, ExportDefaultDeclaration, CallExpression, Expression, Statement } from "@babel/types";
26
26
  import { importSourceModule } from "./source-loader.js";
27
- import type { WorkflowMemoryConfig } from "../types/workflow.js";
27
+ import type { WorkflowMemoryConfig, SnapshotConfig } from "../types/workflow.js";
28
28
  import type { SandboxNetworkPolicy } from "../sandbox.js";
29
29
  import { isWorkflow } from "../workflow-steps/workflow.js";
30
30
  import type { Workflow } from "../workflow-steps/types.js";
@@ -65,15 +65,20 @@ export interface BundledWorkflow {
65
65
  manifest: WorkflowManifest;
66
66
  networkPolicy?: SandboxNetworkPolicy;
67
67
  placeholders?: Record<string, string>;
68
- /** Run UUID, workflow name, or `name@version` referencing the snapshot
69
- * this workflow's runner boots from, if declared via the `snapshot`
70
- * field on `defineWorkflow`. */
71
- snapshot?: string;
72
- /** Default capture-on-success flag from the workflow definition. */
73
- saveSnapshot?: boolean;
68
+ /** Snapshot config from the workflow definition — `bootFrom` (where to
69
+ * restore at run start), `save`, `retain`. */
70
+ snapshots?: SnapshotConfig;
74
71
  workflowPlan: WorkflowPlan;
75
72
  /** Workflow Memory extractor config. */
76
73
  memory?: WorkflowMemoryConfig;
74
+ /** Ordered post-hook workflow names declared on the workflow. */
75
+ postRunHooks?: readonly string[];
76
+ /** Compact JSON-Schema-shaped description of the workflow's input
77
+ * type. Extracted from the workflow's declared `input` zod schema
78
+ * at bundle time; undefined when the schema is `z.unknown()`. */
79
+ inputSchema?: import("../types/workflow-metadata.js").IOSchema;
80
+ /** Same for the workflow's output zod schema. */
81
+ outputSchema?: import("../types/workflow-metadata.js").IOSchema;
77
82
  }
78
83
 
79
84
  async function bundle(path: string, label: string): Promise<string> {
@@ -270,14 +275,76 @@ export async function bundleWorkflow(
270
275
  bundlerVersion: BUNDLER_VERSION,
271
276
  };
272
277
 
278
+ // Input + output schemas: the workflow's `input` / `output` zod
279
+ // schemas → compact JSON-Schema-shaped descriptions carried in
280
+ // template metadata. Used by the dashboard's "Registered" panel to
281
+ // render schema tables and (eventually) client-side validation in
282
+ // the playground. Run-form workflows stamp `z.unknown()` for both
283
+ // — `extractIOSchema` filters those down to undefined so the
284
+ // dashboard renders the "no declared schema" empty state instead
285
+ // of a meaningless empty table.
286
+ const inputSchema = extractIOSchema(workflow.input);
287
+ const outputSchema = extractIOSchema(workflow.output);
288
+
273
289
  return {
274
290
  source,
275
291
  manifest,
276
292
  workflowPlan: plan,
277
293
  networkPolicy: overrides?.networkPolicy ?? metadata.networkPolicy,
278
294
  placeholders: overrides?.placeholders ?? metadata.placeholders,
279
- ...(metadata.snapshot !== undefined ? { snapshot: metadata.snapshot } : {}),
280
- ...(metadata.saveSnapshot !== undefined ? { saveSnapshot: metadata.saveSnapshot } : {}),
281
- ...(metadata.memory !== undefined ? { memory: metadata.memory } : {}),
295
+ ...(inputSchema !== undefined ? { inputSchema } : {}),
296
+ ...(outputSchema !== undefined ? { outputSchema } : {}),
297
+ ...(metadata.snapshots !== undefined ? { snapshots: metadata.snapshots } : {}),
298
+ ...(metadata.memory !== undefined ? { memory: metadata.memory } : {}),
299
+ ...(metadata.postRunHooks !== undefined ? { postRunHooks: metadata.postRunHooks } : {}),
282
300
  };
283
301
  }
302
+
303
+ /** Convert a workflow's `input` or `output` zod schema into a compact,
304
+ * serialisable description for template metadata. Uses Zod 4's
305
+ * `toJSONSchema` and trims the result to fields the dashboard
306
+ * renders — full draft 2020-12 schemas have refs / unions / nested
307
+ * allOf chains that are noisier than the dashboard needs.
308
+ *
309
+ * Returns undefined when:
310
+ * - The schema can't be serialised (corrupt / unknown variant)
311
+ * - The schema is effectively `unknown` (the run-form sugar default —
312
+ * no meaningful contract to render). */
313
+ function extractIOSchema(
314
+ zodSchema: { _zod?: unknown } | unknown,
315
+ ): import("../types/workflow-metadata.js").IOSchema | undefined {
316
+ // Late require to keep this module tree-shakeable for callers that
317
+ // don't bundle. `toJSONSchema` lives on the top-level Zod export in v4.
318
+ let json: Record<string, unknown> | undefined;
319
+ try {
320
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
321
+ const z = require("zod") as { toJSONSchema?: (s: unknown) => Record<string, unknown> };
322
+ if (!z.toJSONSchema) return undefined;
323
+ json = z.toJSONSchema(zodSchema);
324
+ } catch {
325
+ return undefined;
326
+ }
327
+ if (!json || typeof json !== "object") return undefined;
328
+
329
+ // `z.unknown()` serialises to `{}` (no type, no properties). Skip that
330
+ // — there's nothing useful to show.
331
+ if (Object.keys(json).filter((k) => k !== "$schema").length === 0) return undefined;
332
+
333
+ const out: import("../types/workflow-metadata.js").OutputSchema = {};
334
+ if (json.type !== undefined) out.type = json.type as string | string[];
335
+ if (json.description !== undefined) out.description = String(json.description);
336
+ if (json.properties && typeof json.properties === "object") {
337
+ const props: Record<string, { type?: string | string[]; description?: string }> = {};
338
+ for (const [key, raw] of Object.entries(json.properties as Record<string, unknown>)) {
339
+ if (raw && typeof raw === "object") {
340
+ const r = raw as Record<string, unknown>;
341
+ props[key] = {};
342
+ if (r.type !== undefined) props[key].type = r.type as string | string[];
343
+ if (r.description !== undefined) props[key].description = String(r.description);
344
+ }
345
+ }
346
+ if (Object.keys(props).length > 0) out.properties = props;
347
+ }
348
+ if (Array.isArray(json.required)) out.required = json.required.map(String);
349
+ return out;
350
+ }
@@ -23,6 +23,7 @@ import type { z } from "zod";
23
23
  import type { Step, Workflow } from "./types.js";
24
24
  import { WORKFLOW_BRAND } from "./types.js";
25
25
  import { extractMetadata } from "../types/workflow-metadata.js";
26
+ import type { SnapshotConfig, WorkflowMemoryConfig } from "../types/workflow-metadata.js";
26
27
  import type { SandboxNetworkPolicy } from "../sandbox.js";
27
28
  import type { Processor } from "../processors/processor.js";
28
29
 
@@ -46,8 +47,9 @@ export interface StepWorkflowDefinition<TInput, TOutput> {
46
47
  output: z.ZodType<TOutput>;
47
48
  networkPolicy?: SandboxNetworkPolicy;
48
49
  placeholders?: Record<string, string>;
49
- snapshot?: string;
50
- saveSnapshot?: boolean;
50
+ snapshots?: SnapshotConfig;
51
+ memory?: WorkflowMemoryConfig;
52
+ postRunHooks?: readonly string[];
51
53
  processors?: readonly Processor[];
52
54
  }
53
55
 
@@ -18,7 +18,7 @@ export function buildInvokeChild(
18
18
  if (!baseUrl || !apiKey) {
19
19
  throw new Error("ctx.invokeChild requires AGENT_COMPOSE_URL and AGENT_COMPOSE_API_KEY");
20
20
  }
21
- childClient = new AgentComposeClient(baseUrl, apiKey);
21
+ childClient = new AgentComposeClient({ apiKey, baseUrl });
22
22
  return childClient;
23
23
  };
24
24
  return (name, input, childOpts) => getChildClient().invokeAndWait(name, input, {