Adapters
The components never call a model, so something has to turn what your
provider sends into AssistantEvents. Three adapters do, without
importing the provider's SDK: their inputs are duck-typed shapes, so
@zuilib/ai stays dependency-free and the adapters keep working across
SDK versions that share the wire format.
import {fromAISDKStream, fromAISDKUIMessages} from '@zuilib/ai/adapters/ai-sdk'
import {fromAnthropicStream, fromAnthropicMessage, anthropicToolResult} from '@zuilib/ai/adapters/anthropic'
import {fromSSE, parseSSE} from '@zuilib/ai/adapters/sse'
Every from*Stream is an async generator of AssistantEvents that ends
at done, so it is directly a RunResult for an
AssistantDock suggestion or for
useThread().consume.
AI SDK
fromAISDKStream(chunks) reads the UI message stream behind useChat
(v5 chunk objects: text-delta, tool-input-start,
tool-input-available, tool-output-available, tool-output-error,
source-url, error, finish) or the v4 data stream protocol as lines
(0:"…", 9:{…}, a:{…}, 3:"…", d:{…}). A tool is a queued
tool-call at input start and a running one with args once its input
is available; sources are gathered into one citation event before
done.
fromAISDKUIMessage(message) / fromAISDKUIMessages(messages) map finished
UIMessages (v5 parts, with v4 content and tool-invocation
tolerated) to Messages for a Thread. createAISDKMapper()
is the stateful chunk-to-events function and parseUIMessageStreamLine the v4
line parser, for a custom loop.
const {messages} = useChat()
<Thread messages={fromAISDKUIMessages(messages)} />
Anthropic Messages API
fromAnthropicStream(events) reads MessageStreamEvents:
content_block_start (a text block, or a tool_use block that becomes
a queued tool-call), content_block_delta (text_delta → text,
input_json_delta gathered per block), content_block_stop (the tool
call becomes running with its parsed args), message_stop → done,
and error. The model's tool result comes from your app: push
anthropicToolResult(toolUseId, result, {error?, elapsedMs?}) after running
the tool. fromAnthropicMessage(message) maps a finished Message, or a
user turn carrying tool_result content blocks, to a ZUI Message.
createAnthropicMapper() is the stateful event-to-events function.
run: ({signal}) => fromAnthropicStream(client.messages.stream({...}, {signal}))
Server-sent events
parseSSE(source) reads a fetch Response, a
ReadableStream<Uint8Array> or any iterable of strings / bytes into
{event?, data, id?, retry?} messages, handling multi-line data:,
comments and a CRLF split across chunks. fromSSE(source, map?) maps
them to events. The default map: [DONE] → done; an error event →
error; JSON data with a known type is the event as is; JSON with a
delta, text or content string, a JSON string, or plain text under
no event / a text or message event → a text delta; anything else is
skipped. Give map to read your own gateway's format.
run: ({signal}) => fromSSE(fetch('/api/assist', {method: 'POST', body: JSON.stringify(input), signal}))
Writing one
An adapter is a function from your stream to an async iterable of
AssistantEvents (@zuilib/ai/stream-events): {type: 'text', delta},
{type: 'tool-call', id, toolName, args?, status?}, {type: 'tool-result', callId, result?, error?, elapsedMs?}, {type: 'approval', id, title, …},
{type: 'diff', id, original?, modified?, hunks?, patch?}, {type: 'citation', sources}, {type: 'error', message}, {type: 'done', summary?}. Nothing else is required.