Skip to content

Migrating 1.x → 2.0

One breaking change, and it only affects code that reads the capabilities.discovered audit event. Everything else in 2.0 is additive.

Do I need to do anything?

bash
grep -rn "capabilityIds" src/

Nothing found → upgrade and you are done:

bash
pnpm up "@orpc-agent/*@^2"

The @orpc-agent/* packages release in lockstep, so upgrade them together.

The one breaking change

capabilities.discovered used to carry the full list of ids an actor could see. It now carries a count and a digest:

ts
// 1.x
{ type: "capabilities.discovered", surface: "aiSdk",
  data: { capabilityIds: ["orders.search", "orders.refund", /* …297 more */ ] } }

// 2.0
{ type: "capabilities.discovered", surface: "aiSdk",
  data: { count: 299, surface: "aiSdk", digest: "…" } }

Why. The event fires on every describe. A host that composes a tool set per step emitted it per step, per turn, per concurrent user — and at 300 capabilities the id array was roughly 6 KB each time, stored and often forwarded over the wire (ADR-017).

Migrating each usage

You were doingDo this instead
data.capabilityIds.lengthdata.count
Comparing id lists to detect a changeCompare data.digest — equal digests mean equal catalogs
Displaying or storing the idsTurn on verbose (below), or call runtime.describe where you need the list
ts
// Restores data.capabilityIds verbatim, alongside count and digest
const runtime = createAgentRuntime({
  governance,
  audit: { sinks: [auditSink], verbose: true },
});

Before switching verbose on globally, check where those events go. The id list is one actor's authorized surface; forwarding it to a client that is not filtering per actor discloses one user's catalog to another.

Two rules on digest, because the algorithm is deliberately not part of the contract:

  • Compare digests to each other. Equal digest ⇒ equal catalog.
  • Never parse one, reconstruct ids from one, or store one as a durable identifier.

New behaviour worth knowing

describe can now reject with a CapabilityError where only a TypeError was possible before:

ts
try {
  const descriptors = await runtime.describe("aiSdk", { actor, context });
} catch (err) {
  // code: "TIMEOUT", stage: "discovery" — the whole describe exceeded
  // defaults.discoveryBudgetMs (30 s). It throws rather than returning a
  // short catalog, which would be indistinguishable from "this actor lost access".
}

Only reachable if your discovery-phase policies are slow enough to burn 30 seconds across a whole catalog. If that is a real risk for you, the fix is in keeping discovery policies cheap, not in raising the budget.

What you gain

Nothing here requires a change; all of it is opt-in.

scope on describe — narrow a catalog before any discovery policy runs, so a route-scoped tool set stops paying for what it discards:

ts
const tools = await toAISDKTools(runtime, {
  actor, context,
  scope: { tags: ["devices"] },     // billing capabilities' policies never evaluate
});

Two rules: tags is ANY, not ALL, and an untagged capability matches no tags scope — tag what you intend to select, or select it by ids. Scope shapes discovery only; invoke does not consult it, so a capability left out of a scoped describe stays fully invocable by an authorized actor (SI-2). Details: describe's scope.

Bounded discovery. Discovery-phase policies now evaluate defaults.policyConcurrency capabilities at a time (16) under a whole-discovery ceiling of defaults.discoveryBudgetMs (30 s). Both are defaults rather than opt-ins, because each replaces an unbounded behaviour with a bounded one. Set policyConcurrency: 1 if you genuinely want serial evaluation.

Coming from 0.x?

Go through 1.0 first — it is where the API dropped its remaining choices:

0.x1.0
createAgentRuntime({ registry, policies })createAgentRuntime({ governance: defineGovernance({ registry, policies }) })
createAgentRuntime({ warnings: false })Removed. Answer the warning instead: name approvals.coordinator (createInMemoryApprovalCoordinator() is a legitimate answer) or name an audit sink (audit: () => {} states that nothing is recorded)

Both changes exist so that a runtime cannot evaluate a policy list no exported value names — which is what let a deleted approval gate pass a green orpc-agent check (ADR-016). After upgrading, re-run orpc-agent snapshot once: a snapshot taken before 1.0 records no runtime policies, so until you rewrite it the removal check is inert. check prints a notice saying so.

Independent community project — not affiliated with or endorsed by the oRPC maintainers.