npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@agent-compose/sdk

v0.8.7

Published

Client library for agent-compose — define agents, runtimes, and workflows, and invoke them against an agent-compose server.

Readme

@agent-compose/sdk

TypeScript SDK for agent-compose. Use it to:

  • Author workflows that run agent loops inside isolated sandboxes
  • Use or define runtimes that drive a coding agent (Claude Code, Codex, Amp, OpenCode, Cursor, Droid, …) as an agent loop
  • Register, invoke, observe, and cancel workflows over the HTTP API (AgentComposeClient)
  • Manage factories, secrets, API keys, and snapshots programmatically

The hierarchy: a team (shown as a space in the dashboard) owns one or more factories; each factory owns workflow templates, secrets, runs and a drive. Workflows are versioned per (factory, name, version). Every team has a default factory, and every call that takes a factorySlug falls back to it.


Installation

npm install @agent-compose/sdk zod   # zod ^4 is a peer dependency

AgentComposeClient runs on Node ≥ 20 or Bun. Bundling a workflow for registration (bundleWorkflow, agentc register) needs Bun.


Authoring a workflow

A workflow is a typed chain of discrete steps: defineWorkflow({ id, input, output }).step(defineStep(...)).build(). Every step is a durable replay checkpoint: the engine records each step's validated output, so a retry or resume picks up after the last completed step instead of re-running the whole workflow.

// my-workflow.ts
import { defineWorkflow, defineStep, agent, claudeRuntime } from "@agent-compose/sdk";
import { z } from "zod";
import PROMPT from "./prompt.md" with { type: "text" };

const Input  = z.object({ repo: z.string() });
const Output = z.object({ ok: z.boolean(), summary: z.string().optional() });

const review = defineStep({
  name:   "review",
  input:  Input,
  output: Output,
  run: async (ctx) => {
    const result = await agent({
      sandbox: ctx.sandbox!,         // the run's own VM
      runtime: claudeRuntime,
      prompt:  `${PROMPT}\n\nRepository: ${ctx.input.repo}`,
      tools:   ["Bash", "Read", "Edit", "Write", "Grep", "Glob"],
      budget:  { turnsPerIteration: 40, maxIterations: 8 },
    });
    await ctx.setMetadata({ summary: result.lastStatus?.summary });
    return { ok: result.lastStatus?.exit_signal ?? false, summary: result.lastStatus?.summary };
  },
});

export default defineWorkflow({
  id:     "my-workflow",
  input:  Input,
  output: Output,
  // Optional: outbound network rules the runner sandbox enforces. Both
  // providers enforce the host list and inject the headers; only Vercel
  // also enforces per-host method/path rules. `$VAR` placeholders resolve
  // from the workflow's secrets at dispatch, and the value never enters
  // the sandbox.
  networkPolicy: {
    allow: {
      "*": [],
      "api.anthropic.com": [{ transform: [{ headers: { "x-api-key": "$ANTHROPIC_API_KEY" } }] }],
    },
  },
})
  .step(review)
  .build();

The builder chain checks at compile time that each step's input schema matches the previous step's output, and .build() verifies the final step's output is the same Zod instance as the workflow's declared output. networkPolicy, placeholders, snapshots, resources and the other definition fields become metadata the bundler picks up at registration.

The run-form (defineWorkflow({ run(ctx, sandbox) { … } })) has been removed: defineWorkflow throws when it sees a run key. Step-form is the only shape, and pausing only works there.

What a step can do with ctx

Each step's run(ctx) receives a StepContext:

interface StepContext<TInput> {
  input:          TInput;                  // validated against the step's `input` schema
  stepName:       string;
  run:            { id: string };
  sandbox?:       SandboxProvider;         // the run's VM: pass to `agent({ sandbox: ctx.sandbox })`
  abortSignal:    AbortSignal;             // fires when the run is canceled
  requestContext: RequestContext;          // tenant identity, for processors
  agentEvents:    AgentEventSink;          // `agent({ events: ctx.agentEvents })`
  setMetadata(data: Record<string, unknown>): Promise<void>;
  step<T>(name: string, fn: () => Promise<T>): Promise<T>;       // durable named sub-step
  pause<T>(req: PauseRequest<T>): Promise<T>;                    // wait for a human, durably
  sleep(durationMs: number): Promise<void>;
  waitForEvent<T>(req: WaitForEventRequest<T>): Promise<T>;
  requestDecision(req: DecisionRequest): Promise<{ decision: string }>;
  invokeChild(name, input?, opts?): Promise<RunStatus>;          // run another workflow
}

ctx.step("phase-name", () => …) wraps a phase within a step for the run timeline. It memoises the result (a pause-resume re-entry restores it instead of re-running) and emits workflow_substep_started / workflow_substep_completed / workflow_substep_failed lifecycle events with the duration. Use it for setup, external API calls, or anything you want visible on the run page.

The agent loop

agent({
  sandbox,            // ctx.sandbox, the run's VM
  runtime,            // claudeRuntime, codexRuntime, …, or your own via defineRuntime
  prompt,             // raw markdown; a `--- frontmatter ---` block is stripped
  tools?,             // model tool allowlist (defaults inside agentLoop; [] disables tools)
  budget?,            // { turnsPerIteration, maxIterations }; omitted: no turn cap, up to 3 iterations
  workingDir?,        // every shell command runs here (default: the run's dir on the drive)
  responseSchema?,    // zod; when set, the loop demands a `<response>` block on exit
  mode?,              // "auto" (default) or "hitl" (a human can steer it mid-run)
  events?,            // lifecycle sink, e.g. ctx.agentEvents
  onAgentEvent?,      // per-message hook
  onIteration?,       // per-iteration hook with the parsed `<status>` block
  processors?,        // pre/post pipeline around the loop
})
// → AgentLoopResult { agentId, label, sessionId, lastStatus, iterations, response? }

The model emits XML-tagged blocks (<status>, <response>) that the loop parses. lastStatus is the last <status> block: { summary, completed, blockers, exit_signal }. See sdk/src/agent/protocol-suffix.md for the instructions appended to every prompt.


Runtimes

A runtime wraps an agent's execution model (usually a coding CLI) so agent can drive it. The SDK ships built-ins; you only need a custom one for a provider it does not cover.

Built-in runtimes

Each comes as a ready default and a create… factory that takes config:

| Default | Factory | Drives | |---|---|---| | claudeRuntime | createClaudeRuntime | Claude through the Claude Agent SDK, from the runner process | | claudeCodeRuntime | createClaudeCodeRuntime | the Claude Code CLI | | codexRuntime | createCodexRuntime | the Codex CLI | | ampRuntime | createAmpRuntime | Amp | | opencodeRuntime | createOpencodeRuntime | OpenCode | | cursorRuntime | createCursorRuntime | Cursor's cursor-agent CLI | | droidRuntime | createDroidRuntime | Factory's droid CLI | | | createVercelRuntime | a model through the Vercel AI SDK |

openAIDesktopRuntime is not in the package root: it pulls in sharp for screenshots, whose native binding cannot be cross-compiled. Import it from its subpath when you want it:

import openAIDesktopRuntime from "@agent-compose/sdk/runtimes/openai-desktop";

Custom runtime

import { defineRuntime, type AgentMessage } from "@agent-compose/sdk";

export default defineRuntime({
  create: (sandbox, opts) => ({
    // Return a ModelExecutionContract; see sdk/src/types/runtime.ts
    async *sendMessage({ prompt, sessionId, signal }): AsyncGenerator<AgentMessage> {
      // Run the model against `sandbox` and yield AgentMessage chunks the loop parses.
    },
  }),
});

AgentRuntime is { create(sandbox, RuntimeOptions) → ModelExecutionContract }. There is no provider field on it: the runtime is bound to the workflow at author time (you pass it to agent), not selected by the server.


Registering a workflow

The agentc CLI bundles and registers in one step:

agentc register my-workflow.ts -n my-workflow

Under the hood that calls bundleWorkflow(workflowPath), which bundles the source with Bun.build, checks that the default export is a defineWorkflow(...) workflow, and returns the source with a manifest the server verifies against the source hash. Then it posts to /api/v1/factories/<slug>/templates. To register from your own build pipeline (under Bun):

import { AgentComposeClient, bundleWorkflow } from "@agent-compose/sdk";

const client = new AgentComposeClient({
  baseUrl: "https://your-server.example.com",   // default: AGENT_COMPOSE_URL, else https://api.agentcompose.ai
  apiKey:  process.env.AGENT_COMPOSE_API_KEY!,  // default: AGENT_COMPOSE_API_KEY
});

const bundled = await bundleWorkflow("./my-workflow.ts");
await client.register({
  name:         "my-workflow",
  source:       bundled.source,
  manifest:     bundled.manifest,      // required
  workflowPlan: bundled.workflowPlan,
  schedule:     "*/30 * * * *",        // optional cron (UTC)
  factorySlug:  "default",             // optional, defaults to "default"
  // pass bundled.networkPolicy, placeholders, snapshots, resources, … to keep
  // what the workflow declares
});

register() needs the manage scope on the caller's key.


Invoking a workflow

Two flavours:

// Fire-and-forget: returns the run id at once.
const { id } = await client.invoke("my-workflow", {
  repo: "owner/repo",
});

// Block until the run settles (default 30 min timeout, 1 s poll).
const status = await client.invokeAndWait("my-workflow", { repo: "owner/repo" }, {
  timeoutMs:      5 * 60_000,
  pollIntervalMs: 2000,
});
console.log(status.status);   // "success" | "failed" | "abandoned" | "canceled"
console.log(status.output);   // the workflow's return value

output is the final step's return value, validated against the workflow's declared output schema. setMetadata() writes to a separate metadata field, for side-channel facts (a PR URL, a plan URL) that don't belong in the structured return.

invoke and invokeAndWait take { factorySlug, snapshots, networkPolicy, placeholders, size, parentRunId, idempotencyKey } as the third argument. Per-invocation snapshots merges field by field with the registered default. factorySlug defaults to "default".

Auto parent/child tracing

The SDK reads process.env.RUN_ID (set by the runner sandbox on every dispatch) and threads it as parentRunId on invoke() calls. Workflows that fan out to other workflows get a parent/child tree in the dashboard for free. Pass parentRunId: null to opt out.

Cancelling a run

await client.cancelRun(runId);

Idempotent: cancelling a run that already ended returns its current state without throwing. The server marks the run canceled, emits a run_canceled event on the stream and kills its sandbox.

Streaming live events

streamRunLogs returns an async generator of RunEvents in real time, over SSE. Pass lastEventId (the highest seq you have processed) to resume after a reconnect.

for await (const ev of client.streamRunLogs(runId, { lastEventId: 0 })) {
  console.log(ev.event, ev.seq);
  if (ev.event === "run_complete" || ev.event === "run_failed" || ev.event === "run_canceled") {
    break;
  }
}

AbortSignal works too: pass { signal } and call controller.abort() to close the stream from the caller side.


Factories

Factories are containers within a team. Each has its own workflow templates, secrets, runs and drive, and API keys can be limited to one. New projects don't need to think about them: default exists in every team and is what the SDK uses when factorySlug is omitted.

// CRUD on factories
await client.createFactory({ slug: "ci-bots", name: "CI Bots", description: "…" });
const factories = await client.listFactories();
const f         = await client.getFactory("ci-bots");
await client.updateFactory("ci-bots", { name: "Continuous-Integration Bots" });
await client.deleteFactory("ci-bots");

// Templates: across factories, or scoped to one
const all    = await client.listTemplates();
const scoped = await client.listTemplates({ factorySlug: "ci-bots" });

// Register, invoke and secret calls all accept factorySlug
await client.invoke("scrape", { url: "…" }, { factorySlug: "ci-bots" });
await client.setSecret("scrape", "GH_TOKEN", "ghp_…", { factorySlug: "ci-bots" });

CLI equivalents: agentc factory list | create | use | delete | show, plus --factory <slug> on most other commands.


Secrets

Secret values live in GCP Secret Manager and are write-only: the API only returns metadata (key, timestamps). A run sees its factory's secrets plus its workflow's own, the workflow's winning on a clash. A secret the workflow's networkPolicy references as $VAR is brokered: the platform adds it to matching requests at the network layer and the sandbox env holds only a placeholder, or nothing. Any other secret is a plain env var in the runner sandbox.

// Per-workflow
await client.setSecret("my-workflow", "ANTHROPIC_API_KEY", process.env.ANTHROPIC_API_KEY!);
const list = await client.listSecrets("my-workflow"); // [{ key, createdAt, updatedAt }]
await client.deleteSecret("my-workflow", "STALE_KEY");

// Factory-wide (every workflow in the factory inherits them)
await client.setFactorySecret("SLACK_WEBHOOK_URL", "https://hooks.slack.com/…", { factorySlug: "ci-bots" });

Factory secrets need the manage scope. Changing a workflow's secrets needs invoke plus write access to that workflow.


API keys

Mint and list keys programmatically (the caller's key needs admin). A new key is returned once, in the same response as its metadata: copy the ac_… value at once.

const created = await client.createApiKey({
  name:      "ci-dispatcher",
  scopes:    ["read", "invoke"],          // omitted = ["read"]
  expiresAt: new Date(Date.now() + 30 * 86_400_000).toISOString(),  // 30 days
  // factorySlug: "ci-bots"               // optional: limit the key to one factory
});
console.log(created.key);   // "ac_…", the only time you will see it

const page = await client.listApiKeys();

Scopes: read, invoke (dispatch runs), manage (operate a factory: templates, secrets, repos) and admin (members, roles, keys). A key can only mint scopes its holder has. CLI equivalent: agentc keys create <name> --scopes read,invoke --expires-in 30d.


Usage

const usage = await client.getUsage(
  new Date(Date.now() - 30 * 86_400_000),
  new Date(),
);
// usage.data: [{ eventType, unit, total, tags }]

CLI equivalent: agentc usage.


Snapshots (replay-friendly sandboxes)

A workflow can capture its runner sandbox as a snapshot on success (snapshots: { saveLatest: true }), on Vercel or E2B. Other runs boot from it with snapshots.bootFrom into the same prepared VM (deps installed, repo cloned) instead of repeating the setup.

// Capture per-invocation:
await client.invoke("my-workflow", input, { snapshots: { saveLatest: true } });

// Boot from a captured snapshot (take the id from `agentc snapshot list`
// or the dashboard):
await client.invoke("my-workflow", input, {
  snapshots: { bootFrom: { snapshotId: "snap_…" } },
});

// Set a default in the definition:
defineWorkflow({ id, input, output, snapshots: { saveLatest: true } }).step(…).build();

// Keep every step's snapshot (not just the latest):
defineWorkflow({ id, input, output, snapshots: { saveLatest: true, retainSteps: true } }).step(…).build();

// Browse and clean up:
const page  = await client.listSnapshotsPage({ workflow: "my-workflow", limit: 50 });
const snaps = page.data;
await client.deleteRunSnapshot(snaps[0].runId, snaps[0].snapshotId);

CLI equivalents: agentc snapshot list, agentc snapshot show <run-id>, agentc snapshot delete <run-id> [snapshot-id].

Per-invocation overrides merge field by field

The snapshots object on invoke() is merged with the registered template's snapshots config: you can override bootFrom alone without losing saveLatest, or the other way round.


Authentication

The SDK sends a Bearer API key (ac_…). Mint one in the dashboard: sign in, open the space's settings, Credentials → API keys. A new key gets read by default; tick invoke to dispatch runs, manage to register workflows or set secrets, and admin only to mint keys or manage members.

const client = new AgentComposeClient({
  baseUrl: process.env.AGENT_COMPOSE_URL,
  apiKey:  process.env.AGENT_COMPOSE_API_KEY,
});

Both options fall back to those env vars (and the URL to https://api.agentcompose.ai), so new AgentComposeClient() works inside a run sandbox. The dashboard itself uses a session cookie; the SDK is for programmatic and server-to-server callers.


Public exports: quick reference

| Export | What | |---|---| | defineWorkflow | Typed step-workflow builder: defineWorkflow({ id, input, output }).step(...).build() | | defineStep | Declare one typed workflow step ({ name, input, output, run }) | | defineRuntime | Wrap an agent execution provider as an AgentRuntime | | defineSandboxEnvironment | Declare a workflow whose purpose is to build a snapshot for others to boot from | | agent / agentLoop | Embed an agent loop inside a workflow | | runWorkflow | Local engine for running a workflow in-process (test harness) | | bundleWorkflow | Bundle and validate a workflow for registration (Bun) | | claudeRuntime, codexRuntime, … and their create… factories | Built-in runtimes (table above) | | AgentComposeClient | HTTP client: register, invoke, cancel, stream, factories, snapshots, secrets, API keys, usage, conversations, sessions, projects | | AgentComposeError | Thrown by every non-2xx HTTP response | | parseAgentStatus / parseAgentResponse / AgentStatusSchema / AgentMessageSchema | Protocol parsers | | parseSseStream | Generic SSE decoder (used by streamRunLogs) | | createSandbox / reconnectSandbox / killAllSandboxes / killSandboxById / getSandboxQuotas / listOwnedSandboxes / deleteSandboxSnapshot | Sandbox-provider helpers (Vercel and E2B) |

Type exports include Workflow, Step, StepContext, WorkflowBuilder, WorkflowHooks, AgentBudget, AgentRuntime, RuntimeOptions, ModelExecutionContract, McpServerConfig, AgentMessage (and its variants), AgentStatus, RunStatus, RegisterResult, RunEvent, FactoryRow, SnapshotListEntry, ApiKey, ApiKeyCreated, UsageRollupRow, UsageResponse, CancelRunResponse, AgentLoopResult, AgentOpts, SandboxProvider, DesktopSandboxProvider, SandboxNetworkPolicy, SandboxCreateOpts, OwnedSandbox and BundledWorkflow.

For the exact signatures, follow your IDE's go-to-definition into @agent-compose/sdk: sdk/src/index.ts is the public surface, and the files it re-exports from carry full docstrings.


Errors

Every non-2xx HTTP response throws AgentComposeError. It carries status and a message taken from the server's { error: string } body when present (else the HTTP status text), plus code and capability on an authorization denial and retryAfterMs when the server sent Retry-After:

import { AgentComposeError } from "@agent-compose/sdk";

try {
  await client.invoke("missing-workflow");
} catch (err) {
  if (err instanceof AgentComposeError && err.status === 404) {
    // template not registered
  }
}

invokeAndWait throws AgentComposeError(504, …) on timeout.