Vercel codecs

@ably/ai-transport/vercel ships two codecs for the Vercel AI SDK, and which one you need depends on whether you work with a transport or a session.

createUIMessageCodec() returns the wire codec: an encoder and a decoder, and nothing else. It turns UIMessageChunk events into Ably messages and back. The transports take it.

createUIMessageSessionCodec() returns the session codec: the wire codec plus the reducer that merges events into a projection, the getMessages function the conversation tree calls, and the well-known input factories. Every session takes this one, and a wire codec does not satisfy it.

You rarely call either factory yourself. The Vercel-pre-bound createClientSession and createAgentSession already supply the session codec. Call a factory explicitly when you build a codec wrapper, run the encoder or decoder outside a session, or compose one codec into another.

Both are stateless, so hold a single instance at module scope, or in React useMemo(() => createUIMessageSessionCodec(), []), and reuse it rather than calling the factory inline on every render.

The wire codec

createUIMessageCodec<TMetadata, TDataParts, TTools>(): WireCodec<VercelInput, VercelOutput>
JavaScript

1

2

3

4

5

import { createUIMessageCodec } from '@ably/ai-transport/vercel';

// Client code. The transports want the wire codec.
const codec = createUIMessageCodec();
const transport = createClientTransport({ client: ably, channelName, codec });

Properties

adapterTagString or Undefined
Ably-Agent identifier registered on the channel so traffic is attributed to this codec.
createEncoder(channel, options?) => Encoder<VercelInput, VercelOutput>
Create a Vercel encoder bound to the supplied channel writer.
createDecoder() => Decoder<VercelInput, VercelOutput>
Create a Vercel decoder for the channel.

See WireCodec for the generic interface the wire codec satisfies.

The session codec

createUIMessageSessionCodec<TMetadata, TDataParts, TTools>(): Codec<VercelSessionInput, VercelOutput, VercelProjection, UIMessage>

The concrete return type narrows the Codec interface's optional tool factories to the ones this codec actually provides, so all three are present rather than possibly undefined.

JavaScript

1

2

3

4

5

import { createUIMessageSessionCodec } from '@ably/ai-transport/vercel';

// Agent code. Sessions want the session codec.
const codec = createUIMessageSessionCodec();
const session = createAgentSession({ client: ably, channelName, codec });

Properties

Everything the wire codec has, plus the reducer, getMessages, and the input factories.

init() => VercelProjection
Build an empty VercelProjection.
fold(state, event, meta) => VercelProjection
Merge a VercelSessionInput or VercelOutput into the projection.
createEncoder(channel, options?) => Encoder<VercelSessionInput, VercelOutput>
Create a Vercel encoder bound to the supplied channel writer.
createDecoder() => Decoder<VercelSessionInput, VercelOutput>
Create a Vercel decoder for the channel.
getMessages(projection) => CodecMessage<UIMessage>[]
Extract { codecMessageId, message } pairs from a VercelProjection. The domain UIMessage.id is preserved verbatim from the source stream; the SDK correlates messages by the paired codecMessageId.
createUserMessage(message: UIMessage) => VercelSessionInput
Wrap a UIMessage as the UserMessage input variant.
createRegenerate(target, parent) => VercelSessionInput
Build a Regenerate input targeting an assistant UIMessage.
createToolResult(codecMessageId, { toolCallId, output }) => VercelSessionInput
Build a ToolResult input addressed at the assistant codec-message that contains the tool call.
createToolResultError(codecMessageId, { toolCallId, message }) => VercelSessionInput
Build a ToolResultError input.
createToolApprovalResponse(codecMessageId, { toolCallId, approved, reason? }) => VercelSessionInput
Build a ToolApprovalResponse input.

Type parameters

Both factories are generic over the AI SDK's three UIMessage type parameters, as createUIMessageSessionCodec<TMetadata, TDataParts, TTools>(). Supplying concrete types specialises the codec so getMessages, and a session's view.getMessages(), return messages whose metadata, data parts, and tool parts carry your types instead of the SDK defaults; omitting them preserves the default inference. The same parameters apply to createClientSession, createAgentSession, and createChatTransport, and typed messages shows the end-to-end pattern.

VercelInput, VercelSessionInput, VercelOutput and VercelProjection are likewise generic over the same three parameters, each defaulting to the SDK default. The tables below show the default instantiation.

TInputVercelMessageInput | VercelRegenerateInput | VercelChunkInput | VercelApprovalInput
The wire union: every record-shape a client publishes on the ai-input wire, as the wire codec sees them. Each arm is discriminated on kind, and the message, chunk and approval arms carry their body on payload. The regenerate arm carries no body, because the transport's publish options carry its regenerates and parent structure instead.
TInputUserMessage<UIMessage> | Regenerate | ToolResult<VercelToolResultPayload> | ToolResultError<VercelToolResultErrorPayload> | ToolApprovalResponse<VercelToolApprovalResponsePayload>
The session union, composed from the SDK's well-known input variants, with the tool variants parameterised by the Vercel domain payload shapes (VercelToolResultPayload, VercelToolResultErrorPayload, VercelToolApprovalResponsePayload). This is the union a session's send accepts.
TOutputAI.UIMessageChunk
Every record-shape the agent publishes on the ai-output wire. The codec passes Vercel's UIMessageChunk through unchanged.
TProjectionVercelProjection
Per-run projection. Carries the { codecMessageId, message } pair list, per-message stream-tracker state, and any tool resolutions that arrived before their assistant message. The reducer merges unconditionally and keeps no high-water-mark, because deduplication happens a layer above it. Opaque: the SDK owns the shape. Use getMessages.

The well-known input variants (UserMessage, Regenerate, ToolResult, ToolResultError, ToolApprovalResponse) are documented on the Codec reference page. The Vercel codecs layer no extra input variants.

Example

Decode a single Ably message and merge the resulting events into a fresh projection. Merging needs the session codec, because only that codec has init, fold and getMessages. ReducerMeta.serial is required and carries ordering context only, so the reducer never treats it as a replay marker. The messageId field is optional and only needed when the codec routes events to an existing message.

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

import { createUIMessageSessionCodec } from '@ably/ai-transport/vercel';

const codec = createUIMessageSessionCodec();
const decoder = codec.createDecoder();
let projection = codec.init();

channel.subscribe((message) => {
  if (!message.serial) return; // live channel-subscribe messages always carry one
  const { inputs, outputs } = decoder.decode(message);
  for (const event of inputs) {
    projection = codec.fold(projection, { direction: 'input', event }, { serial: message.serial });
  }
  for (const event of outputs) {
    projection = codec.fold(projection, { direction: 'output', event }, { serial: message.serial });
  }
  render(codec.getMessages(projection).map((entry) => entry.message));
});