Ecosystem interop

Four agentic runtimes describe the same two things — a tool call and a tree of components — in words of their own. This is the mapping, verified against each one's published package.
Import
JSX
import { toolPart, applyToolEvent } from '@box-kite/react/interop';
import { a2uiApply, a2uiSurface, a2uiToSpec, a2uiCatalog } from '@box-kite/react/interop';
import { fromGenerativeUi, toGenerativeUi } from '@box-kite/react/interop';

One vocabulary for where a tool call is

toolPart(part) reads a part from AI SDK, assistant-ui or CopilotKit and answers in this library’s words, so the same two components draw a turn whichever runtime produced it. The split worth knowing is that a decision is not a stage a call passes through — it is a question somebody answers, which is why two components cover what AI SDK reports as six states.
RuntimeWhat it reportsWhere it lands
AI SDK 6/7input-streaming · input-available · output-available · output-errorToolCallCard
AI SDK 6/7approval-requested · approval-respondedApprovalCard
assistant-uirunning · complete · incompleteToolCallCard
assistant-uirequires-actionApprovalCard
CopilotKitinProgress · executing · completeToolCallCard
CopilotKitexecuting, holding a respondApprovalCard
AG-UITOOL_CALL_START · _ARGS · _END · _RESULTToolCallCard
AG-UIINTERRUPTApprovalCard
AG-UI is the odd one out, and it is worth knowing why: it reports events rather than parts, so its cards need a fold instead of a mapping. applyToolEvent(parts, event) is that fold — one call per event, oldest first, keyed by toolCallId.
JSX
import { ApprovalCard, ToolCallCard } from '@box-kite/react/components/agent';
import { toolPart } from '@box-kite/react/interop';

function Part({ part, respond }: { part: unknown; respond: (approved: boolean) => void }) {
  const mapped = toolPart(part);

  if (!mapped) return null;
  if (mapped.kind === 'approval') {
    return <ApprovalCard title="Refund order 4182" onDecisionChange={(d) => respond(d === 'approved')} />;
  }

  return <ToolCallCard name="searchOrders" status={mapped.status} input={mapped.input} output={mapped.output} />;
}

A2UI: a flat list of components, as a tree

A2UI is the protocol Google published and CopilotKit and Oracle converged on, and its wire shape is the only one here that is genuinely different: a flat adjacency list of components referring to each other by id, arriving one message at a time, with a data model of its own per surface. a2uiApply folds the stream; a2uiToSpec walks one surface into the tree <SpecRenderer> takes.
JSX
import { catalog } from '@box-kite/react/catalog';
import { a2uiApply, a2uiEmpty, a2uiSurface, a2uiToSpec } from '@box-kite/react/interop';
import SpecRenderer, { createSpecRegistry } from '@box-kite/react/spec';

const allowed = catalog({ include: ['Flex', 'H2', 'P'] });
const registry = createSpecRegistry({ catalog: allowed, components: { Flex, H2, P } });

function Surface({ onMessage }: { onMessage: (fold: (message: unknown) => void) => void }) {
  const [state, setState] = useState(a2uiEmpty);

  // One message at a time, folded in as the transport delivers it.
  onMessage((message) => setState((current) => a2uiApply(current, message)));

  const surface = a2uiSurface(state);

  return <SpecRenderer spec={a2uiToSpec(surface, { catalog: allowed })} registry={registry} data={surface?.data} />;
}
Three things the two models already agreed on, each a special case that did not have to be written:
  • Its data binding is already ours. { "path": "/user/email" } is a JSON Pointer, and $data has taken one since it was written — so a binding is a rename rather than a parse.
  • A template is a repeat. children: { componentId, path } is one node per item of an array, which is exactly what repeat means.
  • A half-arrived surface is the ordinary case. An agent streams a leaf before the branch that holds it, so a component nothing reaches from the root is simply not rendered yet — and a cycle in the id graph is cut rather than walked.
The other direction is a2uiCatalog(catalog()): the same components and the same values their props take, in the shape an adjacency list needs — a component carries its own id, its type is a property rather than the key above it, and its children are ids rather than nested nodes. Serve it at the catalogId you gave it, which is what an agent’s createSurface names.

assistant-ui: the same idea, arrived at twice

Its GenerativeUISpec and this library’s SpecNode differ in three ways, and they are the whole adapter: the name is component rather than type, a child may be a bare string that renders as text, and their nodes carry no data binding, no repeat and no action channel — in their design those are the tool’s job rather than the spec’s. So toGenerativeUi reports what it could not carry rather than emitting a tree that renders half a view in silence.
JSX
const { spec, losses } = toGenerativeUi(node);
// losses: [{ code: 'data-binding' | 'repeat' | 'event', path }]

CopilotKit: two surfaces, opposite directions

Its A2UI renderer takes a catalog built from Zod schemas, and z.fromJSONSchema is the whole bridge — which is why catalog() emits JSON Schema at all. One trap, measured against zod 4.6: a $ref resolves against the document, not against the piece you lifted out of it, so converting a component on its own throws Reference not found: #/$defs/color. a2uiComponentSchema(document, name) is that component with the document’s definitions attached.
Its actions go the other way: render is a call being shown and renderAndWaitForResponse is a question, whose respond is what onDecisionChange calls.
JSX
useCopilotAction({
  name: 'refundOrder',
  parameters: [{ name: 'orderId', type: 'number' }],
  renderAndWaitForResponse: ({ args, respond }) => (
    <ApprovalCard title={'Refund order ' + args.orderId} onDecisionChange={(d) => respond?.(d === 'approved')} />
  ),
});

Verified against the packages, not against a memory of them

Every runtime named here is a devDependency of the repository, and the adapters are checked against the published .d.ts and the published functions. A shape read off a blog post is a shape that has already moved.
ClaimChecked against
toGenerativeUi emits their GenerativeUISpec
@assistant-ui/core
fromGenerativeUi reads their GenerativeUINode
@assistant-ui/core
every ToolCallMessagePartStatus maps to a card
@assistant-ui/core
a2uiCatalog is a document createCatalog accepts
@copilotkit/a2ui-renderer
a surface generated against it renders here
the fold, end to end
the catalog validates a spec written against it
@json-render/react
The two cases A2UI’s own conformance suite keeps for a message processor — that an update lands on the surface it names and nowhere else, and that each surface owns its data model — are in the test suite in the vocabulary that suite uses.

What is deliberately not here

A wrapper around somebody else’s runtime
This entry imports none of the four. An adapter that pulled one in would be choosing a runtime for the app, and the app has already chosen — so what ships is the half this library owns: the mapping, framework-free, engine-free, and importable from a route handler and a client alike.