Migrating 1.x → 2.0
One breaking change, and it only affects code that reads the
capabilities.discoveredaudit event. Everything else in 2.0 is additive.
Do I need to do anything?
grep -rn "capabilityIds" src/Nothing found → upgrade and you are done:
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:
// 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 doing | Do this instead |
|---|---|
data.capabilityIds.length | data.count |
| Comparing id lists to detect a change | Compare data.digest — equal digests mean equal catalogs |
| Displaying or storing the ids | Turn on verbose (below), or call runtime.describe where you need the list |
// 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:
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:
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.x | 1.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.
Related
- Roadmap — what shipped in each release
- ADR-017 — why discovery changed
- Release process — what a major means here