The SDK is a pnpm monorepo. Every package is ESM-only and strictly
typed against TypeScript 5.6 with exactOptionalPropertyTypes.
@arcp/sdk — meta, re-exports the three below; ships `arcp` CLI
├── @arcp/client — ARCPClient (consumer side)
├── @arcp/runtime — ARCPServer + Job + Lease + BearerVerifier
└── @arcp/core — wire primitives shared by client + runtime
Middleware packages are optional adapters around @arcp/core's
Transport contract:
@arcp/node — Node http.Server WS upgrade
@arcp/express — Express app + DNS-rebind protection
@arcp/fastify — Fastify upgrade
@arcp/hono — Hono app for @hono/node-server
@arcp/bun — Bun.serve({ websocket }) wrapper
@arcp/middleware-otel — OTel span emission + W3C trace propagation
The shared kernel: nothing in here knows about being a client or a runtime. It exports:
- Envelope schema — Zod definitions for every message type, plus
Envelopeas a discriminated union and helpers likebuildEnvelope(),messageEnvelope(),isPreSessionType(). - Branded IDs —
JobId,SessionId,MessageId,TraceId, andEventSeqare brand types; you cannot mix them up by accident, and schema validation enforces format constraints (e.g.,TraceIdmust be 32 lowercase hex chars). - Error taxonomy — one class per code in
ERROR_CODES; the base isARCPError. All errors carry a structuredErrorPayloadfor wire emission. - Transports —
Transportinterface plus three implementations:MemoryTransport(pair viapairMemoryTransports()),StdioTransport,WebSocketTransport+startWebSocketServer(). - Session state —
SessionStateis the phase machine (pre-handshake→awaiting-welcome→accepted→closed), andPendingRegistrytracks awaiting request/response correlations. - Event log —
EventLoginterface with a SQLite-backed default for replay during resume (§6.3). - Auth verifier —
BearerVerifierinterface andStaticBearerVerifierfor tests. - Capability negotiation —
negotiateCapabilities()computes the intersection of advertised features, withV1_1_FEATURESas the default opt-in set.
See packages/core.md.
A single class: ARCPClient. It owns one transport at a time, drives
the handshake, manages the pending-request map, dispatches inbound
envelopes to registered handlers, and exposes:
connect(transport)— handshake; returns the welcome payload.resume(transport, resumeInfo)— single-use token, replay events.submit(opts)— issuejob.submit, awaitjob.accepted, return aJobHandlewhose.doneresolves on the terminal result.subscribe(jobId, opts)— v1.1, attach to a job from a second session (§6.6).listJobs(filter)— v1.1, paginated listing (§6.6).cancelJob(jobId)— cooperative cancellation with 30-second grace.ack(seq)— v1.1 back-pressure ack.
See packages/client.md.
Two main classes: ARCPServer and SessionContext. ARCPServer
holds the agent registry and shared resources (event log, bearer
verifier, idempotency cache); SessionContext is created per
accepted transport and runs the per-session machinery (dispatch,
heartbeat, ack tracking, subscription set).
Agents are functions registered by name:
server.registerAgent("name", async (input, ctx) => {
await ctx.status("running");
await ctx.log("info", "starting");
return { ok: true };
});The JobContext (ctx) is the agent's window into the runtime: it
emits all eight reserved job.event kinds plus vendor extensions,
exposes the immutable lease, surfaces the cancellation AbortSignal,
and (v1.1) provides budget tracking, lease constraints, and a
streaming ResultStream for chunked results (§8.4).
Job is a value object describing a single in-flight job. The
runtime transitions it through
pending → running → {success|error|cancelled|timed_out} (§7.3).
See packages/runtime.md.
Meta-package. Drop-in replacement for the pre-split
@agentruntimecontrolprotocol/sdk:
import { ARCPClient, ARCPServer, pairMemoryTransports } from "@arcp/sdk";It also ships the arcp CLI binary — see cli.md.
Middleware packages don't add protocol semantics; they're adapters
that produce a Transport from a host framework. The runtime never
knows it's running inside Express vs Fastify vs Bun — it just sees
transports flow through server.accept(transport).
The OTel middleware is different: it wraps an existing transport, injecting W3C trace context into envelope extensions on send and emitting spans for each frame. Use it when you want per-job spans in your observability stack.
See packages/ for one page per middleware.
Every message is one JSON object with a small fixed envelope:
| Field | Required | Notes |
|---|---|---|
arcp |
always | "1.1" (the v1.1 wire-format version literal) |
id |
always | ULID or UUIDv7, unique per message |
type |
always | discriminator ("session.hello", "job.submit", …) |
payload |
always | type-specific body |
session_id |
after handshake | absent on pre-session frames |
job_id |
job-scoped envelopes | set on job.* types |
event_seq |
job.event / job.result / job.error |
strictly monotonic per session |
trace_id |
optional | W3C 32-hex |
extensions |
optional | x-vendor.*-keyed extension object |
Anything else on the wire is ignored. Unknown x-vendor.* types are
round-tripped per §15 — see vendor-extensions.md.
- Envelope schemas:
packages/core/src/envelope.tsandpackages/core/src/messages/ - Transports:
packages/core/src/transport/ - Client:
packages/client/src/client.ts - Server + dispatch:
packages/runtime/src/server.ts - Job + JobContext:
packages/runtime/src/job.ts - Lease validation:
packages/runtime/src/lease.ts