agentwire

@kovenlabs/agentwire-ai-sdk

The Vercel AI SDK adapter — tool conversion and the useAgentChat runtime.

pnpm add @kovenlabs/agentwire-ai-sdk

Peer dependencies: ai (^5 || ^6), @ai-sdk/react (^2 || ^3, client only), react. This is the only package that imports the AI SDK — swap it to target a different SDK.

Two entry points:

  • @kovenlabs/agentwire-ai-sdk — server-safe (toAiTool, toAiToolSet), no React.
  • @kovenlabs/agentwire-ai-sdk/react — the client hooks + renderer.

Exports

@kovenlabs/agentwire-ai-sdk (server-safe)

ExportKindSummary
toAiToolfnone AgentToolDefinition → AI SDK Tool
toAiToolSetfna record of definitions → ToolSet
ToAiToolOptionstype{ logger? }
AgentToolContexttype{ userId?, chatId?, agent? } from experimental_context

@kovenlabs/agentwire-ai-sdk/react

ExportKindSummary
useAgentChathookthe chat runtime over useChat
UseAgentChatOptionstypeuseAgentChat options
ToolHandlertype(call, addToolOutput) => void for client tools
AddToolOutput / AddToolOutputArgstypethe settle callback + its args
INTERACTIVE_RESOLVE_CHANNELconstdefault resolve channel
completeInteractivefnsettle as completed
interruptInteractivefnsettle as interrupted
resolveInteractivefnsettle with a raw output
InterruptOptionstype{ reason?, channel? }
useInteractiveResulthookobserve one tool's resolutions
useInteractiveResultStatehook…returning the latest as state
UseInteractiveResultOptionstype{ includeInterrupts?, channel? }
useToolResulthookobserve a server tool's result
useToolResultStatehook…returning the latest as state
AgentMessagescomponentrender the message list
renderMessagePartsfnrender one message's parts
AgentMessagesPropstype<AgentMessages> props
RenderPartsOptionstyperenderMessageParts options
ToolPartSlotstypeper-part render overrides
InteractiveRegistrytypeRecord<toolName, Widget>
ToolWidgetPropstype{ toolCallId, input } a widget receives
ApprovalRespondertypeaddToolApprovalResponse shape

toAiTool / toAiToolSet

Convert neutral AgentToolDefinitions into Vercel AI SDK Tools. server/approval tools get their execute wrapped with start/finish/fail logging; approval tools carry needsApproval. client/ interactive tools are emitted without execute.

app/api/agent/chat/route.ts
import { toAiToolSet } from "@kovenlabs/agentwire-ai-sdk";
import { consoleLogger } from "@kovenlabs/agentwire";
import { streamText } from "ai";
import { tools } from "@/lib/tools";

const result = streamText({
  model,
  messages,
  tools: toAiToolSet(tools, { logger: consoleLogger }),
  experimental_context: { chatId, agent, userId },
});

toAiToolSet(defs, options?) maps a whole record; toAiTool(def, options?) converts a single definition. options is ToAiToolOptions ({ logger? }, defaulting to noopLogger). The logger receives AgentToolContext (userId, chatId, agent, read from experimental_context) on every tool log line, so executions are correlated.

useAgentChat

The client runtime. Wraps the AI SDK's useChat and wires it to the bus:

  • settles interactive tool calls from a single bus channel, deduped by toolCallId (works for live and history-loaded calls);
  • dispatches client tools to your toolHandlers;
  • publishes tool:<name>:result when server tools complete (seeded on load so already-settled history calls don't re-fire side effects);
  • suppresses auto-continuation after terminal tools.

It returns the standard useChat object (messages, sendMessage, status, …).

components/chat.tsx
"use client";

import { useAgentChat } from "@kovenlabs/agentwire-ai-sdk/react";

const chat = useAgentChat({
  chatId,
  agent: "atlas",
  toolHandlers: {
    navigate: (call, addToolOutput) => {
      router.push((call.input as { to: string }).to);
      addToolOutput({ tool: "navigate", toolCallId: call.toolCallId, output: { ok: true } });
    },
  },
  terminalTools: ["suggestFollowUps"],
});

Options

FieldDefaultPurpose
chatId—Chat id; also the useChat id.
agent"orchestrator"Sent in the request body.
api"/api/agent/chat"Chat endpoint.
initialMessages—History to hydrate.
toolHandlers—Record<name, (call, addToolOutput) => void> for client tools.
interactiveToolsregistry's interactive toolsNames settled via the bus.
resolveChannelINTERACTIVE_RESOLVE_CHANNELChannel the UI publishes results/interrupts on.
terminalTools[]Tools that end the turn (no auto-continue after).
publishServerToolResultstruePublish tool:<name>:result on completion.
onDanglingToolCalls—Sanitize history-loaded messages (e.g. close stuck calls).
logger / telemetryno-opInjected logging/telemetry.

Each toolHandlers entry is a ToolHandler: (call: { toolName, toolCallId, input }, addToolOutput: AddToolOutput) => void, where AddToolOutput takes AddToolOutputArgs ({ tool, toolCallId, output }).

Settling an interactive call

Use the helpers — they publish the right payload on the resolve channel and the runtime forwards it to addToolOutput.

import { completeInteractive, interruptInteractive } from "@kovenlabs/agentwire-ai-sdk/react";

// the traveler picked a flight → builds { status: "completed", flightId }
completeInteractive("pickFlight", toolCallId, { flightId });

// the traveler bailed out to chat → builds { status: "interrupted", reason, tool }
interruptInteractive("pickFlight", toolCallId);

completeInteractive(tool, toolCallId, data?, channel?) and interruptInteractive(tool, toolCallId, extra?, { reason?, channel? }) assemble the completed / interrupted output shapes. For a custom output shape, drop to resolveInteractive(tool, toolCallId, output, channel?), which passes output through verbatim. All default to INTERACTIVE_RESOLVE_CHANNEL.

Consuming a resolution off-chat — useInteractiveResult

Any component (a side panel, a map) can react to one interactive tool's resolutions without touching the chat. The hook filters by tool name, skips interrupts by default, and hands you the typed output — no manual payload.tool === name check.

import { useInteractiveResult } from "@kovenlabs/agentwire-ai-sdk/react";

useInteractiveResult<{ flightId: string; price: number }>("pickFlight", (flight) => {
  setSelected(flight); // fires from anywhere, whenever the traveler picks
});
Arg / optionPurpose
toolNameonly this tool's resolutions fire the handler.
handler(output, payload)output is typed by the type arg; payload is the raw { tool, toolCallId, output }.
includeInterruptsalso fire for interrupted resolutions (default false).
channelresolve channel to listen on (default INTERACTIVE_RESOLVE_CHANNEL).

Consuming a server tool's result — useToolResult

The counterpart for server/approval tools, which auto-publish on tool:<name>:result. It hides that channel convention and the cast.

import { useToolResult } from "@kovenlabs/agentwire-ai-sdk/react";

useToolResult<Flight[]>("searchFlights", (flights) => setFlights(flights));

Both hooks are thin adapters over useSubscribe — the subscription mechanism lives in one place; these just add the filter and types.

State variants

When you only need the latest value, useInteractiveResultState / useToolResultState return it directly (T | null) and skip the useState plumbing. Same arguments as the callback hooks, minus the handler.

const flights = useToolResultState<Flight[]>("searchFlights"); // Flight[] | null
const picked = useInteractiveResultState<{ flightId: string }>("pickFlight");

Rendering — <AgentMessages>

A headless renderer that turns chat.messages into UI: it mounts your interactive widgets from a registry, renders Approve/Deny for approval tools, and shows a status-then-output progression for the rest. See it in the full example.

import { AgentMessages } from "@kovenlabs/agentwire-ai-sdk/react";

<AgentMessages
  chat={chat}
  interactive={{ pickFlight: FlightGallery, collectPassenger: PassengerForm }}
  slots={{ text: (t) => <p>{t}</p> }}
/>;
PropPurpose
chatthe useAgentChat result (needs messages; uses addToolApprovalResponse for approvals).
interactiveRecord<toolName, Widget> — mounted at the input-available state with { toolCallId, input }.
slotsoptional overrides: text, status, output, error, approval.
renderMessagewrap each message (defaults to a <div>).

Approval tools (defineTool.approval) are auto-detected from the AI SDK's approval-requested state and answered with chat.addToolApprovalResponse (ApprovalResponder) — no prop needed.

Render types: AgentMessagesProps (the props above), InteractiveRegistry (Record<toolName, ComponentType<ToolWidgetProps>>), ToolWidgetProps ({ toolCallId, input }), ToolPartSlots (the slots overrides: text/status/output/error/approval), and ApprovalResponder.

renderMessageParts

The headless function <AgentMessages> is built on — renderMessageParts(message, options) returns the rendered parts for one UIMessage. Use it directly for a fully custom message layout. options is RenderPartsOptions:

OptionPurpose
interactiveInteractiveRegistry — widgets mounted at input-available.
addToolApprovalResponseanswers approval tools (pass chat.addToolApprovalResponse).
slotsthe ToolPartSlots render overrides.
import { renderMessageParts } from "@kovenlabs/agentwire-ai-sdk/react";

chat.messages.map((m) => (
  <div key={m.id}>{renderMessageParts(m, { interactive, addToolApprovalResponse: chat.addToolApprovalResponse, slots })}</div>
));

Version compatibility

useAgentChat accesses ai's approval-completion helper defensively, so it works on both ai@5 (no approval auto-continue) and ai@6 (approval responses auto-continue). No code change needed across the two.