Codec
A Codec turns your framework's events into Ably messages, and turns Ably messages back into events. An agent adaptor uses one codec for everything it sends and pipes, and a decoder uses the same codec for everything it receives. The Vercel AI SDK, OpenAI and AG-UI codecs ship with the SDK, and defineCodec builds a codec for any other event type.
A codec only defines the wire format. The adaptor writes to the channel, and the decoder delivers the decoded events.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createAgentAdaptor, createDecoder, defineCodec } from '@ably/ai-transport';
const chatCodec = defineCodec({
id: 'my-chat',
typeOf: (event) => event.type,
events: {
prompt: {
encode: (event) => ({ data: event.text }),
decode: ({ data }) => ({ type: 'prompt', text: String(data) }),
},
},
});
// channel is the conversation's Ably channel.
const adaptor = createAgentAdaptor(channel, chatCodec);
const decoder = createDecoder(chatCodec);Properties
idStringextras.ai.codec, so a decoder can tell this codec's messages from another codec's on the same channel.nameStringid.encodeFunction returning an array of EncodedMessage(event: E) => EncodedMessage[]. Encode one event to the Ably messages that carry it, in the order the adaptor writes them. An empty array publishes nothing. Throws for an event the codec does not know.decodeFunction(message: Ably.InboundMessage) => E[]. Decode one Ably message to the events it carries, in order. Returns an empty array for a message that carries no event. Throws for a message the codec recognises but cannot decode.The decoder handles a decode that throws: it reports the error to its onError handlers, and delivers the message with no event.
A message with none of publish, append or update is a plain publish that nothing appends to. send() has no keys to append to, so it rejects an event the codec encodes as an append or an update. Use pipe() for streams.
Build a codec
defineCodec<E, K extends string>(config: DefineCodecConfig<E, K>): Codec<E>Build a codec from a table with one row for each event type. Each row has an encode function that returns the message body and where it goes, and a decode function that rebuilds the event from the body. A row produces at most one message and at most one event.
The builder writes the row's data as the message body, and the codec's id, the event type and the row's fields under extras.ai. It writes the row's headers under extras.headers. On decode it reads them back and picks the row by the event type.
An append only adds to a message's data, so a field that a streaming event needs on every delta goes in fields.
The decoder also handles the three ways Ably delivers a streamed message. A live append carries only its new text. The first delivery after an attach carries the full content so far, as an update. A history read returns the whole message. The decoder reduces all three to the text that delivery adds before the row's decode runs, and drops a message it has already seen, so history and live delivery can overlap without duplicates.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { defineCodec } from '@ably/ai-transport';
const chatCodec = defineCodec({
id: 'my-chat',
typeOf: (event) => event.type,
events: {
prompt: {
encode: (event) => ({ data: event.text }),
decode: ({ data }) => ({ type: 'prompt', text: String(data) }),
},
'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) }),
},
},
});Parameters
configrequiredDefineCodecConfigReturns
Codec<E>. The codec. defineCodec throws an ErrorInfo with InvalidArgument when a row key is a wildcard with no prefix. A built codec's encode throws InvalidArgument for an event type with no row, or for a row that sets more than one of publish, append and update. Its decode throws InvalidArgument for a message whose type has no row.
Example
A codec for a chat application whose agent streams text. A prompt is a plain publish. Each text delta appends to one message per stream, keyed by the stream id, and the end event forgets the key.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Shared by the agent and the client.
import { createDecoder, defineCodec } from '@ably/ai-transport';
import { createClient } from '@ably/pubsub-device';
const chatCodec = defineCodec({
id: 'my-chat',
typeOf: (event) => event.type,
events: {
prompt: {
encode: (event) => ({ data: event.text }),
decode: ({ data }) => ({ type: 'prompt', text: String(data) }),
},
'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) }),
},
},
});
const ably = createClient({ authUrl: '/auth' });
const decoder = createDecoder(chatCodec);
decoder.onEvent(({ event }) => {
if (event?.type === 'text-delta') appendText(event.id, event.delta);
});
await ably.channels.get('conversations:42').subscribe(decoder.receive);