Appearance
Adapter: Vercel AI SDK
Status: Implemented in v0.1. Package:
@orpc-agent/ai-sdk. Peer:ai@^5,@orpc-agent/core.
Converts a runtime's capabilities into AI SDK v5 tools. Surface id: aiSdk. One capability exposed to aiSdk ⇢ one tool; the adapter is thin by contract (adapter model) — discovery, validation, policy, approval, and execution all happen in the runtime.
Usage
ts
import { generateText, stepCountIs } from "ai";
import { toAISDKTools } from "@orpc-agent/ai-sdk";
// Inside your authenticated request handler — build tools PER REQUEST:
const tools = await toAISDKTools(runtime, {
actor: sessionActor(req), // authenticated identity, never model-derived (SI-3)
context: await createAppContext(req),
});
const result = await generateText({
model, // any provider — the adapter never touches it
system: SUPPORT_SYSTEM_PROMPT,
messages,
tools,
stopWhen: stepCountIs(8),
});Options (AISDKToolsOptions)
| Option | Required | Notes |
|---|---|---|
actor | yes | Bound into every call of this tool set |
context | yes | The app's oRPC context for this request |
filter | no | (descriptor) => boolean; conversation-shaping only, not authorization (SI-2) |
toolNaming | no | Default .→_ (orders.refund → orders_refund); meta.adapters.aiSdk.toolName overrides per capability; collisions throw at build |
signal | no | Composed into every invocation (in addition to per-call abort from the loop) |
toAISDKTools awaits runtime.describe("aiSdk", { actor, context }), so the returned set is already exposure- and discovery-policy-filtered for this actor. Build per request; caching a tool set across users leaks visibility decisions.
What each generated tool contains
description— frommeta.description(plus the suffix" Requires approval."when the descriptor saysrequiresApproval— cheap, honest model guidance).inputSchema— the capability's input schema converted to JSON Schema (via@orpc-agent/core/schema) and wrapped with the AI SDK'sjsonSchema()helper. The adapter does not pre-validate;executeforwards raw arguments so the runtime remains the single validation authority (pipeline stage 5).execute(args, { abortSignal })— callsruntime.invoke(capabilityId, args, { actor, context, surface: "aiSdk", signal })and translates the envelope.
Result shape
Tool results are always a structured envelope (AISDKToolResult) — deterministic for the model, uniform across capabilities:
jsonc
// completed
{ "status": "ok", "data": { /* redacted output */ } }
// approval-required
{ "status": "approval-required", "approvalId": "apr_9",
"message": "Awaiting approval: Refund of $649 exceeds $500." }
// failed / cancelled — exposeToModel errors
{ "status": "error", "error": { "code": "POLICY_DENIED",
"message": "Refunds of $5000 or more cannot be issued by agents.", "retryable": false } }
// failed — concealed errors (SI-9)
{ "status": "error", "error": { "code": "INTERNAL_ERROR",
"message": "The operation failed.", "retryable": false } }Design choices, stated:
- Return, don't throw. Throwing inside
executeproduces provider-dependent tool-error handling; a returned envelope keeps the model in the loop with typed, uniform feedback it can reason about (retry input validation, stop on denial, report approval state). INPUT_INVALIDincludeserror.details(issue paths) so the model can self-correct — the one case where details cross the boundary, because the model authored the data.approval-requiredis not an error: the model's correct behavior is to inform the user and stop, and the shape says so.
Approval flow in a chat loop
The tool result cannot wait for a human (tool calls should return promptly; approvals may take hours). Pattern:
text
model calls orders_refund → { status: "approval-required", approvalId }
model tells user; loop ends this turn
… human approves in your dashboard (runtime.approvals.decide) …
your app calls runtime.resume(approvalId, { context }) from the dashboard/worker
app posts the outcome into the conversation (or the user asks again and the
model re-invokes — new execution, new decision, consumed approvals don't re-fire)The inline approvals.handler mode exists for short-latency confirmation UIs (the human is present and the transport can hold the call open) — see guides/human-approval.md.
Host loops with their own approval UX
Agent frameworks that consume AI SDK tools (Mastra, and others) often ship their own tool-approval mechanism — a pre-execution "approve this tool call?" gate rendered by the host's stream. When a governed runtime sits underneath, pick one authority, and it must be the runtime:
- Host-loop approval decides on the raw tool-call arguments, before
invoke: no canonical input hash (SI-5), no coordinator record, nocapability.approval_requested/approvedaudit events, no expiry, no self-approval check. For governed operations that is a shadow approval system your audit trail cannot see. - Running both gates double-prompts: the host asks, then the runtime's policy suspends the same call again.
So: do not enable the host's tool approval for governed tools. Let orpc-agent's policies decide, and render the approval-required envelope — it is a typed tool result (AISDKToolResult), so your UI can render an approve/deny card generically from the stream's tool-result parts (or from a pending-approvals fetch), then call your decide + resume endpoints. The Mastra task board example is this exact wiring, working.
For requester-confirmed gates where the human is present in the chat, the composition from the ADR-006 addendum fits streaming UIs: an inline approvals.handler holds the call open for human-confirmation types and returns undefined for everything else, deferring manager-type approvals to the coordinator flow unchanged.
Cancellation
The AI SDK's per-call abortSignal flows into runtime.invoke, composes with the capability timeout, and reaches the handler (SI-12). Aborting the chat request cancels in-flight capability calls; the audit trail shows capability.cancelled.
Testing
Don't test through a model. @orpc-agent/testing invokes with surface: "aiSdk" to assert exposure/policy behavior for this surface, and adapter conversion is covered by the conformance checklist (adapters/testing.md).