Codec architecture

Internal architecture of the codec: the encode and decode contract, how defineCodec builds a codec from an event table, and how the decoder core turns appends, updates and history into the events a client receives.

A codec turns a framework's events into Ably messages and back. It holds the wire format and nothing else. The transport owns the channel, keeps the table that turns a stream key into the serial an append targets, and delivers the decoded events.

The codec contract

A Codec has two functions and an optional tag:

  • encode(event) returns the Ably messages that carry one event, in order, each with where it goes: a plain publish, a publish or append under a key, an update of the message under a key, and an ends that forgets a key.
  • decode(message) returns the events one Ably message carries, in order. An empty array is a message that is not the codec's, or one that adds nothing.
  • adapterTag names the codec in the channel's params.agent attribution.

One codec covers both directions of a conversation. The events a client sends, such as a user message, and the events the agent pipes are members of the same event type, so the agent and every client use the same codec.

The contract admits any number of messages per event and any number of events per message, so a hand-written codec can split a payload that is too large for one message.

Build a codec from an event table

defineCodec builds a codec from typeOf, which reads an event's type, and one row for each type. Each row has an encode and a decode, which speak the same body:

Body fieldOn the wireUse it for
dataThe message body.The text a streamed event appends, or a plain publish's whole payload.
fieldsextras.ai.fieldsEvent fields that must stay out of a body that appends grow, such as a stream id.
headersextras.headersFields you want Ably's server-side filtering to read.

The builder writes the row's type under extras.ai.type, and picks the row by it on decode. A row produces at most one message and at most one event. A key ending in -* is a wildcard row for every type with that prefix, and an exact key takes priority over a wildcard.

A streamed event keeps its varying text in data and its identifying fields in fields. An append carries only data, so fields are the only way an event's other fields reach a client, and Ably keeps the extras of the last append on the stored message.

Decode appends, updates and history

Ably delivers a streamed message in three shapes: a live append with its fragment, a full-content update after an attach, and the whole message from history. The decoder core reduces all three to one shape before a row's decode runs, so data is always the text that delivery adds.

For each streamed message, the core keeps the text it has passed on and the newest version.serial it has seen:

  • A delivery with a version the core has already seen decodes to nothing. History can overlap live delivery without duplicating text.
  • A full-content update decodes to the part of the text the core has not passed on yet, so a client that missed appends receives the rest in one event.
  • A delete makes the core forget the message.

The core tracks only streamed messages: a message the transport marked with extras.ai.stream, or any serial it first meets through an append or an update. The message that ends a stream carries the stream's serial under extras.ai.ends, and the core forgets the stream when it sees it. A plain publish is passed on and never tracked.

History reads newest page first, so a stream's end message can arrive before the stream's message when the two sit on different pages. The core remembers such an end message, and passes the stream's message on whole when it arrives.

A codec built with defineCodec uses the decoder core for you. A hand-written codec can use it through createDecoderCore.

The codecs that ship with the SDK

The Vercel codec, the OpenAI codec and the AG-UI codec are all built with defineCodec. Each carries its framework's own events unchanged, plus one event of its own for what a client sends: user-message for the Vercel codec, input for the OpenAI codec, and run-input for the AG-UI codec. Each appends its framework's text streams, and publishes every other event as a plain message. The AG-UI codec changes some events on the wire, which its API reference lists.

Write a custom codec

A custom codec follows the same pattern. The custom codec quickstart builds one end to end:

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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

const chatCodec = defineCodec({
  typeOf: (event) => event.type,
  events: {
    'text-start': {
      encode: (event) => ({ fields: { id: event.id } }),
      decode: ({ fields }) => ({ type: 'text-start', id: String(fields.id) }),
    },
    'text-delta': {
      encode: (event) => ({ data: event.delta, fields: { id: event.id }, append: event.id }),
      decode: ({ data, fields }) => ({ type: 'text-delta', id: String(fields.id), delta: String(data) }),
    },
    'text-end': {
      encode: (event) => ({ fields: { id: event.id }, ends: event.id }),
      decode: ({ fields }) => ({ type: 'text-end', id: String(fields.id) }),
    },
  },
});

Let the stream's first delta open the message with append, rather than opening it from the start event. Ably replaces a message's name and extras with those of the latest append, so a message opened by a start event would read back from history under the delta's type.