@agent-compose/sdk 0.1.0 → 0.2.1

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.
@@ -28,8 +28,8 @@ function defineWorkflow(def) {
28
28
  return Object.assign(def.run, {
29
29
  networkPolicy: def.networkPolicy,
30
30
  placeholders: def.placeholders,
31
- sandboxEnvironment: def.sandboxEnvironment,
32
- snapshot: def.snapshot
31
+ snapshot: def.snapshot,
32
+ saveSnapshot: def.saveSnapshot
33
33
  });
34
34
  }
35
35
  // src/sandbox.ts
@@ -350,7 +350,7 @@ function defineSandboxEnvironment(env) {
350
350
  throw new Error(`defineSandboxEnvironment(${env.name}): 'setup' must be a function`);
351
351
  }
352
352
  return defineWorkflow({
353
- snapshot: env.snapshot ?? true,
353
+ saveSnapshot: env.saveSnapshot ?? true,
354
354
  run: () => env.setup(makeLocalSandboxProvider())
355
355
  });
356
356
  }
@@ -367,18 +367,74 @@ class AgentComposeError extends Error {
367
367
  }
368
368
  }
369
369
 
370
+ // src/sse.ts
371
+ async function* parseSseStream(body) {
372
+ const reader = body.getReader();
373
+ const decoder = new TextDecoder;
374
+ let buffer = "";
375
+ let event = "";
376
+ let seq = 0;
377
+ let dataLines = [];
378
+ try {
379
+ while (true) {
380
+ const { done, value } = await reader.read();
381
+ if (done)
382
+ break;
383
+ buffer += decoder.decode(value, { stream: true });
384
+ const lines = buffer.split(`
385
+ `);
386
+ buffer = lines.pop() ?? "";
387
+ for (const line of lines) {
388
+ if (line.startsWith("id:")) {
389
+ seq = Number(line.slice(3).trim());
390
+ continue;
391
+ }
392
+ if (line.startsWith("event:")) {
393
+ event = line.slice(6).trim();
394
+ continue;
395
+ }
396
+ if (line.startsWith("data:")) {
397
+ dataLines.push(line.slice(5).trim());
398
+ continue;
399
+ }
400
+ if (line === "" && dataLines.length > 0) {
401
+ try {
402
+ const data = JSON.parse(dataLines.join(`
403
+ `));
404
+ yield { id: seq, event, data };
405
+ } catch {}
406
+ event = "";
407
+ seq = 0;
408
+ dataLines = [];
409
+ }
410
+ }
411
+ }
412
+ } finally {
413
+ reader.releaseLock();
414
+ }
415
+ }
416
+
370
417
  // src/client.ts
371
418
  var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
419
+ var DEFAULT_FACTORY = "default";
372
420
  function detectAmbientParentRunId() {
373
421
  const envRunId = typeof process !== "undefined" ? process.env?.RUN_ID : undefined;
374
422
  return envRunId && UUID_REGEX.test(envRunId) ? envRunId : null;
375
423
  }
424
+ function templatePath(factorySlug, ...rest) {
425
+ const tail = rest.length > 0 ? "/" + rest.map(encodeURIComponent).join("/") : "";
426
+ return `/api/v1/factories/${encodeURIComponent(factorySlug)}/templates${tail}`;
427
+ }
376
428
 
377
429
  class AgentComposeClient {
378
430
  fetch;
431
+ baseUrl;
432
+ apiKey;
379
433
  constructor(baseUrl, apiKey) {
434
+ this.baseUrl = baseUrl.replace(/\/$/, "");
435
+ this.apiKey = apiKey;
380
436
  this.fetch = ofetch.create({
381
- baseURL: baseUrl.replace(/\/$/, ""),
437
+ baseURL: this.baseUrl,
382
438
  headers: { Authorization: `Bearer ${apiKey}` },
383
439
  async onResponseError({ response }) {
384
440
  const body = response._data;
@@ -387,16 +443,21 @@ class AgentComposeClient {
387
443
  });
388
444
  }
389
445
  register(payload) {
390
- return this.fetch("/api/v1/templates", { method: "POST", body: payload });
446
+ const { factorySlug = DEFAULT_FACTORY, ...body } = payload;
447
+ return this.fetch(templatePath(factorySlug), { method: "POST", body });
391
448
  }
392
449
  invoke(name, input, opts) {
393
450
  const parentRunId = opts?.parentRunId === undefined ? detectAmbientParentRunId() : opts.parentRunId;
394
- return this.fetch(`/api/v1/templates/${name}/invoke`, {
451
+ const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
452
+ return this.fetch(templatePath(factorySlug, name, "invoke"), {
395
453
  method: "POST",
396
454
  body: {
397
455
  input,
398
456
  ...opts?.snapshot !== undefined ? { snapshot: opts.snapshot } : {},
399
- ...parentRunId ? { parentRunId } : {}
457
+ ...opts?.saveSnapshot !== undefined ? { saveSnapshot: opts.saveSnapshot } : {},
458
+ ...parentRunId ? { parentRunId } : {},
459
+ ...opts?.networkPolicy !== undefined ? { networkPolicy: opts.networkPolicy } : {},
460
+ ...opts?.placeholders !== undefined ? { placeholders: opts.placeholders } : {}
400
461
  }
401
462
  });
402
463
  }
@@ -405,7 +466,11 @@ class AgentComposeClient {
405
466
  const pollMs = opts?.pollIntervalMs ?? 1000;
406
467
  const { id: runId } = await this.invoke(name, input, {
407
468
  ...opts?.snapshot !== undefined ? { snapshot: opts.snapshot } : {},
408
- ...opts?.parentRunId !== undefined ? { parentRunId: opts.parentRunId } : {}
469
+ ...opts?.saveSnapshot !== undefined ? { saveSnapshot: opts.saveSnapshot } : {},
470
+ ...opts?.parentRunId !== undefined ? { parentRunId: opts.parentRunId } : {},
471
+ ...opts?.factorySlug !== undefined ? { factorySlug: opts.factorySlug } : {},
472
+ ...opts?.networkPolicy !== undefined ? { networkPolicy: opts.networkPolicy } : {},
473
+ ...opts?.placeholders !== undefined ? { placeholders: opts.placeholders } : {}
409
474
  });
410
475
  const deadline = Date.now() + timeoutMs;
411
476
  while (Date.now() < deadline) {
@@ -432,19 +497,78 @@ class AgentComposeClient {
432
497
  getStatus(runId) {
433
498
  return this.fetch(`/api/v1/workflows/${runId}/status`);
434
499
  }
435
- async listTemplates() {
436
- const body = await this.fetch("/api/v1/templates");
500
+ async listTemplates(opts) {
501
+ const path = opts?.factorySlug ? templatePath(opts.factorySlug) : "/api/v1/templates";
502
+ const body = await this.fetch(path);
437
503
  return body.templates;
438
504
  }
439
- setSecret(workflowName, key, value) {
440
- return this.fetch(`/api/v1/templates/${workflowName}/secrets`, { method: "POST", body: { key, value } });
505
+ async listFactories() {
506
+ const body = await this.fetch("/api/v1/factories");
507
+ return body.factories;
441
508
  }
442
- async listSecrets(workflowName) {
443
- const body = await this.fetch(`/api/v1/templates/${workflowName}/secrets`);
509
+ createFactory(payload) {
510
+ return this.fetch("/api/v1/factories", { method: "POST", body: payload });
511
+ }
512
+ getFactory(slug) {
513
+ return this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}`);
514
+ }
515
+ updateFactory(slug, updates) {
516
+ return this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}`, { method: "PATCH", body: updates });
517
+ }
518
+ deleteFactory(slug) {
519
+ return this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}`, { method: "DELETE" });
520
+ }
521
+ setSecret(workflowName, key, value, opts) {
522
+ const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
523
+ return this.fetch(templatePath(factorySlug, workflowName, "secrets"), {
524
+ method: "POST",
525
+ body: { key, value }
526
+ });
527
+ }
528
+ async listSecrets(workflowName, opts) {
529
+ const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
530
+ const body = await this.fetch(templatePath(factorySlug, workflowName, "secrets"));
444
531
  return body.secrets.map((s) => ({ key: s.secretKey, createdAt: s.createdAt, updatedAt: s.updatedAt }));
445
532
  }
446
- deleteSecret(workflowName, key) {
447
- return this.fetch(`/api/v1/templates/${workflowName}/secrets/${key}`, { method: "DELETE" });
533
+ deleteSecret(workflowName, key, opts) {
534
+ const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
535
+ return this.fetch(templatePath(factorySlug, workflowName, "secrets", key), { method: "DELETE" });
536
+ }
537
+ createApiKey(input) {
538
+ return this.fetch("/api-keys", { method: "POST", body: input });
539
+ }
540
+ async listApiKeys() {
541
+ const body = await this.fetch("/api-keys");
542
+ return body.data;
543
+ }
544
+ getUsage(from, to) {
545
+ const qs = `?from=${encodeURIComponent(from.toISOString())}&to=${encodeURIComponent(to.toISOString())}`;
546
+ return this.fetch(`/api/v1/usage${qs}`);
547
+ }
548
+ cancelRun(runId) {
549
+ return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/cancel`, { method: "POST" });
550
+ }
551
+ async* streamRunLogs(runId, opts) {
552
+ const headers = { Authorization: `Bearer ${this.apiKey}` };
553
+ if (opts?.lastEventId && opts.lastEventId > 0) {
554
+ headers["Last-Event-ID"] = String(opts.lastEventId);
555
+ }
556
+ const res = await fetch(`${this.baseUrl}/api/v1/workflows/${encodeURIComponent(runId)}/stream`, {
557
+ headers,
558
+ ...opts?.signal ? { signal: opts.signal } : {}
559
+ });
560
+ if (!res.ok || !res.body) {
561
+ let message = res.statusText;
562
+ try {
563
+ const body = await res.json();
564
+ if (body.error)
565
+ message = body.error;
566
+ } catch {}
567
+ throw new AgentComposeError(res.status, message);
568
+ }
569
+ for await (const ev of parseSseStream(res.body)) {
570
+ yield ev.data;
571
+ }
448
572
  }
449
573
  }
450
574
  // src/utils/errors.ts
@@ -499,16 +623,16 @@ async function bundleWorkflow(workflowPath, overrides) {
499
623
  const wfBits = await extractFromBundle(source, "workflow", (wf) => wf ? {
500
624
  networkPolicy: wf.networkPolicy,
501
625
  placeholders: wf.placeholders,
502
- sandboxEnvironment: wf.sandboxEnvironment,
503
- snapshot: wf.snapshot
626
+ snapshot: wf.snapshot,
627
+ saveSnapshot: wf.saveSnapshot
504
628
  } : undefined);
505
- const { networkPolicy, placeholders, sandboxEnvironment, snapshot } = wfBits ?? {};
629
+ const { networkPolicy, placeholders, snapshot, saveSnapshot } = wfBits ?? {};
506
630
  return {
507
631
  source,
508
632
  networkPolicy: overrides?.networkPolicy ?? networkPolicy,
509
633
  placeholders: overrides?.placeholders ?? placeholders,
510
- ...sandboxEnvironment ? { sandboxEnvironment } : {},
511
- ...snapshot !== undefined ? { snapshot } : {}
634
+ ...snapshot !== undefined ? { snapshot } : {},
635
+ ...saveSnapshot !== undefined ? { saveSnapshot } : {}
512
636
  };
513
637
  }
514
638
  // src/utils/schemas.ts
package/dist/sse.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Generic Server-Sent Events (SSE) parser.
3
+ *
4
+ * Consumes a `ReadableStream<Uint8Array>` and yields parsed events one at a
5
+ * time. Each event carries:
6
+ * - `id`: numeric (from `id:` line; `0` if absent)
7
+ * - `event`: event name (from `event:` line; `""` if absent)
8
+ * - `data`: parsed JSON payload (the SDK's stream events are always JSON)
9
+ *
10
+ * Malformed payloads are silently skipped — same behaviour as the previous
11
+ * CLI-side parser. Caller drives the loop via `for await (...)` and is
12
+ * responsible for breaking on terminal events.
13
+ *
14
+ * Output type stays loose (`Record<string, unknown>`) on purpose: typed
15
+ * unions like `RunEvent` aren't structurally narrowable from
16
+ * `Record<string, unknown>`, so callers that want a discriminated union
17
+ * cast at the consumption site (see `streamRunLogs`). Keeping the parser
18
+ * payload-agnostic also means it's reusable for future SSE endpoints with
19
+ * different shapes.
20
+ */
21
+ export declare function parseSseStream(body: ReadableStream<Uint8Array>): AsyncGenerator<{
22
+ id: number;
23
+ event: string;
24
+ data: Record<string, unknown>;
25
+ }>;
@@ -70,6 +70,11 @@ export type RunEvent = {
70
70
  at: number;
71
71
  seq?: number;
72
72
  reason: string;
73
+ } | {
74
+ event: "run_canceled";
75
+ runId: string;
76
+ at: number;
77
+ seq?: number;
73
78
  } | {
74
79
  event: "workflow_log";
75
80
  runId: string;
@@ -1,15 +1,16 @@
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. Snapshots are **opt-in**: the wrapper below sets `snapshot: true`
5
- * by default (opt out with `snapshot: false` if you only want side effects).
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).
6
7
  *
7
- * Once captured, reference the snapshot from another workflow's
8
- * `sandboxEnvironment` field by run UUID, workflow name, or `name@version`:
8
+ * Once captured, reference the snapshot from another workflow's `snapshot`
9
+ * field by run UUID, workflow name, or `name@version`:
9
10
  *
10
- * sandboxEnvironment: "my-setup" // most recent successful snapshot
11
- * sandboxEnvironment: "my-setup@v1" // version-scoped recency
12
- * sandboxEnvironment: "<run-uuid>" // pinned to an exact run
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
13
14
  *
14
15
  * `defineSandboxEnvironment` is sugar over `defineWorkflow` — it makes the
15
16
  * setup recipe read like an imperative script by supplying the local
@@ -19,7 +20,7 @@
19
20
  * 1. Author a setup file with `defineSandboxEnvironment`.
20
21
  * 2. `agentc register setup.ts --build` — registers and invokes once to
21
22
  * capture the snapshot.
22
- * 3. Other workflows declare `sandboxEnvironment: "name"` and boot from it.
23
+ * 3. Other workflows declare `snapshot: "name"` and boot from it.
23
24
  * 4. `agentc snapshot list` / `delete` to manage the Vercel storage bill.
24
25
  *
25
26
  * @example
@@ -40,9 +41,9 @@ export interface SandboxEnvironmentDefinition {
40
41
  name: string;
41
42
  description?: string;
42
43
  setup: (sb: SandboxProvider) => Promise<void>;
43
- /** Override the sugar's `snapshot: true` default. Set `false` to opt out
44
- * of snapshot capture (rarely useful — an env with no snapshot can't be
45
- * referenced as `sandboxEnvironment`). */
46
- snapshot?: boolean;
44
+ /** Override the sugar's `saveSnapshot: true` default. Set `false` to opt
45
+ * out of snapshot capture (rarely useful — an env with no snapshot can't
46
+ * be referenced as the `snapshot` field on another workflow). */
47
+ saveSnapshot?: boolean;
47
48
  }
48
49
  export declare function defineSandboxEnvironment(env: SandboxEnvironmentDefinition): WorkflowFn<void>;
@@ -52,19 +52,21 @@ export type WorkflowFn<T = unknown> = (ctx: WorkflowCtx, sandbox: SandboxProvide
52
52
  export interface WorkflowDefinition<T = unknown> {
53
53
  run: WorkflowFn<T>;
54
54
  /**
55
- * Name (or `name@version`) of another workflow whose built snapshot this
56
- * workflow's runner boots into. Any workflow registered with `--build`
57
- * can be used as a sandbox environment.
55
+ * Reference to a snapshot the runner should boot from at run start.
56
+ * Accepts a run UUID, a workflow name, or `name@version` — any workflow
57
+ * registered with `--build` produces a snapshot you can name here. The
58
+ * runner VM starts in that pre-configured state. Per-invocation
59
+ * `invoke({ snapshot })` overrides this default.
58
60
  */
59
- sandboxEnvironment?: string;
61
+ snapshot?: string;
60
62
  /**
61
- * Capture a snapshot of the sandbox on successful /complete. Snapshots are
62
- * long-lived (never auto-expire) — customers list + delete them explicitly
63
- * via `agentc snapshot list/delete`. Per-invocation `invoke({ snapshot })`
64
- * overrides this default. `defineSandboxEnvironment` sugar sets this to
65
- * true by default.
63
+ * Capture a snapshot of the sandbox on successful /complete. Snapshots
64
+ * are long-lived (never auto-expire) — customers list + delete them
65
+ * explicitly via `agentc snapshot list/delete`. Per-invocation
66
+ * `invoke({ saveSnapshot })` overrides this default.
67
+ * `defineSandboxEnvironment` sugar sets this to true by default.
66
68
  */
67
- snapshot?: boolean;
69
+ saveSnapshot?: boolean;
68
70
  /**
69
71
  * Outbound network policy for the runner sandbox.
70
72
  * Use "*": [] to allow all traffic while still injecting headers for specific domains.
@@ -100,7 +102,7 @@ export interface WorkflowDefinition<T = unknown> {
100
102
  * The returned function is a valid WorkflowFn with metadata fields attached
101
103
  * for the bundler / registration layer to read.
102
104
  */
103
- export declare function defineWorkflow<T = unknown>(def: WorkflowDefinition<T>): WorkflowFn<T> & Pick<WorkflowDefinition, "networkPolicy" | "placeholders" | "sandboxEnvironment" | "snapshot">;
105
+ export declare function defineWorkflow<T = unknown>(def: WorkflowDefinition<T>): WorkflowFn<T> & Pick<WorkflowDefinition, "networkPolicy" | "placeholders" | "snapshot" | "saveSnapshot">;
104
106
  /** Observability-only lifecycle hooks — passed to the workflow engine, not workflow authors. */
105
107
  export interface WorkflowHooks {
106
108
  onStepStart?: (step: string) => void;
@@ -13,11 +13,12 @@ export interface BundledWorkflow {
13
13
  source: string;
14
14
  networkPolicy?: SandboxNetworkPolicy;
15
15
  placeholders?: Record<string, string>;
16
- /** Name (or `name@version`) of another workflow whose built snapshot this
17
- * workflow boots from, if declared via `sandboxEnvironment`. */
18
- sandboxEnvironment?: string;
19
- /** Default snapshot-on-success flag from the workflow definition, if any. */
20
- snapshot?: boolean;
16
+ /** Run UUID, workflow name, or `name@version` referencing the snapshot
17
+ * this workflow's runner boots from, if declared via the `snapshot`
18
+ * field on `defineWorkflow`. */
19
+ snapshot?: string;
20
+ /** Default capture-on-success flag from the workflow definition. */
21
+ saveSnapshot?: boolean;
21
22
  }
22
23
  /** Bundle a workflow from source. */
23
24
  export declare function bundleWorkflow(workflowPath: string, overrides?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-compose/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Client library for agent-compose — define agents, runtimes, and workflows, and invoke them against an agent-compose server.",
5
5
  "license": "MIT",
6
6
  "repository": {