OpenAI codecs

@ably/ai-transport/openai ships two codecs for the OpenAI Responses API, and which one you need depends on whether you work with a transport or a session.

ResponsesCodec is the wire codec: an encoder and a decoder, and nothing else. It turns a ResponseStreamEvent stream into Ably messages and back. The transports take it.

ResponsesSessionCodec is 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.

Both are single values rather than factories, so you pass the value itself and never call it. Neither takes type parameters, and one instance serves every session in the process.

The wire codec

JavaScript

1

2

3

4

5

// Client code. The transports want the wire codec.
import { createClientTransport } from '@ably/ai-transport';
import { ResponsesCodec } from '@ably/ai-transport/openai';

const transport = createClientTransport({ channel, codec: ResponsesCodec });

Properties

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

See WireCodec for the generic interface the wire codec satisfies.

The session codec

On the agent, bind it to the session:

JavaScript

1

2

3

4

5

6

7

8

9

// Agent code.
import { createAgentSession } from '@ably/ai-transport';
import { ResponsesSessionCodec } from '@ably/ai-transport/openai';

const session = createAgentSession({
  client: ably,
  channelName: invocation.sessionName,
  codec: ResponsesSessionCodec,
});

On the client, the same value goes to the React provider:

JavaScript

1

2

3

4

5

6

7

8

'use client';

import { ClientSessionProvider } from '@ably/ai-transport/react';
import { ResponsesSessionCodec } from '@ably/ai-transport/openai';

<ClientSessionProvider channelName="conversations:demo" codec={ResponsesSessionCodec}>
  <Chat />
</ClientSessionProvider>

You rarely call the methods below yourself. A session calls init, fold, createEncoder, createDecoder, and getMessages for you. The methods you do call are createUserMessage, to send a user turn, and the three tool factories, to answer a tool call from the client.

Properties

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

init() => OpenAIProjection
Build an empty OpenAIProjection.
fold(state, event, meta) => OpenAIProjection
Merge an OpenAISessionInput or OpenAIOutput into the projection.
createEncoder(channel, options?) => Encoder<OpenAISessionInput, OpenAIOutput>
Create an OpenAI encoder bound to the supplied channel writer.
createDecoder() => Decoder<OpenAISessionInput, OpenAIOutput>
Create an OpenAI decoder for the channel.
getMessages(projection) => CodecMessage<OpenAIMessage>[]
Extract { codecMessageId, message } pairs from an OpenAIProjection.
createUserMessage(message: OpenAIMessage) => OpenAISessionInput
Wrap an OpenAIMessage as the UserMessage input variant.
createRegenerate(target, parent) => OpenAISessionInput
Build a Regenerate input targeting an assistant message.
createToolResult(codecMessageId, payload) => OpenAISessionInput
Build a ToolResult input addressed at the assistant message that holds the function_call. Payload keyed by call_id.
createToolResultError(codecMessageId, payload) => OpenAISessionInput
Build a ToolResultError input for a tool that failed. Payload keyed by call_id.
createToolApprovalResponse(codecMessageId, payload) => OpenAISessionInput
Build a ToolApprovalResponse input answering a gated call. Payload keyed by call_id.

Tool payloads

The three tool factories take a payload keyed by OpenAI's snake_case call_id, taken from the function_call being answered. The Vercel codec uses toolCallId for the same purpose, so the two are not interchangeable.

call_idString
The call_id of the function_call this result answers.
outputResponses.ResponseInputItem.FunctionCallOutput['output']
The tool's output, either text or a content list. Exactly the function_call_output.output shape, so it reaches the model unchanged.
call_idString
The call_id of the function_call that failed.
messageString
Human-readable description of the failure. The reducer merges it into the function_call_output.output, so it becomes the output the model reads on the next turn.
call_idString
The call_id of the gated function_call.
approvedBoolean
Whether the user approved the tool execution.
reasonString
Optional human-readable reason, typically supplied on a denial.

An approval records a decision and produces no output, so the agent runs an approved call server-side when the run resumes. A denial needs no server execution, because the reducer merges in a rejection function_call_output on the client. Conversation helpers covers reading that state back.

Types

role'user' | 'assistant'
Whether the message is the user's prompt or the assistant's reply.
itemsOpenAIItem[]
The message's items, in wire order. An assistant message can hold several, for example reasoning followed by a function_call.
toolCallStatesRecord<string, OpenAIToolCallState>
Approval and client-execution state, keyed by call_id. Present only when the message holds at least one gated or client-executed tool call, because OpenAI's item model has no field for either.
OpenAIItemResponseOutputMessage | ResponseReasoningItem | ResponseFunctionToolCall | ResponseInputItem.FunctionCallOutput | ResponseInputItem.Message
One item inside a message. Every member is a valid Responses API input item, which is why a stored conversation round-trips to /responses unchanged.
approval'pending' | 'approved' | 'denied'
A gated call's approval status, set when the agent requests approval and updated by the client's response.
result'ok' | 'failed'
The client-side execution status, set once a tool result or a tool error merges in.
nameString
The tool name, carried on the approval request.
argumentsString
The tool arguments as JSON text, carried on the approval request.
reasonString
Optional reason accompanying an approval decision.
type'tool-approval-request'
Discriminator.
call_idString
The call_id of the function_call this approval gates.
nameString
The tool's name, so your UI can render the prompt from the request alone, before the function_call finishes streaming.
argumentsString
The tool's arguments as JSON text, mirroring the function_call.
OpenAIInputOpenAIMessageInput | OpenAIRegenerateInput | OpenAIItemInput | OpenAIApprovalInput
The wire union: every record-shape a client publishes on the ai-input wire, as the wire codec sees them. Discriminated on kind.
OpenAISessionInputUserMessage<OpenAIMessage> | Regenerate | ToolResult<OpenAIToolResultPayload> | ToolResultError<OpenAIToolResultErrorPayload> | ToolApprovalResponse<OpenAIToolApprovalResponsePayload>
The session union, composed from the SDK's well-known input variants with the tool variants parameterised by the OpenAI payload shapes. This is the union a session's send accepts, and what the factories above return.
OpenAIOutputResponseStreamEvent | { type: 'function_call_output', item } | ToolApprovalRequestEvent
Every record-shape the agent publishes on the ai-output wire. The codec passes OpenAI's own ResponseStreamEvent through, and adds two events of its own: function_call_output, because a Responses stream never carries a tool's output, and tool-approval-request, because the Responses API has no equivalent.

The event inventory is total, so an event outside it throws at the encoder rather than being dropped. An agent that enables a hosted tool (web or file search, code interpreter, image generation, MCP, custom tools) or audio must filter those events out of the stream before piping it, as Scope and trade-offs describes.

OpenAIProjectionOpenAIProjection
Per-run projection of a node's messages, in publication order. Opaque: the SDK owns the shape and compacts it on read. Use getMessages to get { codecMessageId, message } pairs.

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

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.

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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

const decoder = ResponsesSessionCodec.createDecoder();
let projection = ResponsesSessionCodec.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 = ResponsesSessionCodec.fold(projection, { direction: 'input', event }, { serial: message.serial });
  }
  for (const event of outputs) {
    projection = ResponsesSessionCodec.fold(projection, { direction: 'output', event }, { serial: message.serial });
  }
  render(ResponsesSessionCodec.getMessages(projection).map((entry) => entry.message));
});
  • Conversation helpers: toResponsesInput and the correlation readers the agent loop uses.
  • OpenAI Responses: how the codec fits an agent, including the tool and approval flow.
  • Codec: the generic Codec interface and the well-known input variants.