# Ecosystem interop

_Box Kite 2.1.0 · a markdown copy of https://www.box-kite.dev/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.

| Runtime | What it reports | Where it lands |
| --- | --- | --- |
| AI SDK 6/7 | `input-streaming · input-available · output-available · output-error` | `ToolCallCard` |
| AI SDK 6/7 | `approval-requested · approval-responded` | `ApprovalCard` |
| assistant-ui | `running · complete · incomplete` | `ToolCallCard` |
| assistant-ui | `requires-action` | `ApprovalCard` |
| CopilotKit | `inProgress · executing · complete` | `ToolCallCard` |
| CopilotKit | `executing, holding a respond` | `ApprovalCard` |
| AG-UI | `TOOL_CALL_START · _ARGS · _END · _RESULT` | `ToolCallCard` |
| AG-UI | `INTERRUPT` | `ApprovalCard` |

**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.

| Claim | Checked 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.

---

_Every page: https://www.box-kite.dev/llms.txt · every prop, measured: https://www.box-kite.dev/props.md_

