ResponsesCodec

ResponsesCodec is the pre-built codec for the OpenAI Responses API. It implements Codec<OpenAIInput, OpenAIOutput, OpenAIProjection, OpenAIMessage>, so a session encodes a ResponseStreamEvent stream out and decodes it back into OpenAIMessage objects without a custom implementation.

It is a single codec value, so you pass ResponsesCodec itself and never call it. It takes no type parameters, and one instance serves every session in the process.

On the agent, bind it to the session:

JavaScript

1

2

3

4

5

6

7

8

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

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

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 { ResponsesCodec } from '@ably/ai-transport/openai';

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

You rarely call the methods below yourself. A session drives 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

init() => OpenAIProjection
Build an empty OpenAIProjection.
fold(state, event, meta) => OpenAIProjection
Fold an OpenAIInput or OpenAIOutput into the projection.
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.
getMessages(projection) => CodecMessage<OpenAIMessage>[]
Extract { codecMessageId, message } pairs from an OpenAIProjection.
createUserMessage(message: OpenAIMessage) => OpenAIInput
Wrap an OpenAIMessage as the UserMessage input variant.
createRegenerate(target, parent) => OpenAIInput
Build a Regenerate input targeting an assistant message.
createToolResult(codecMessageId, payload) => OpenAIInput
Build a ToolResult input addressed at the assistant message that holds the function_call. Payload keyed by call_id.
createToolResultError(codecMessageId, payload) => OpenAIInput
Build a ToolResultError input for a tool that failed. Payload keyed by call_id.
createToolApprovalResponse(codecMessageId, payload) => OpenAIInput
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 folds 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 it resumes. A denial needs no server execution, because the reducer folds 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 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 with no conversion.
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 folds 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 a client renders the prompt without waiting for the streamed function_call.
argumentsString
The tool's arguments as JSON text, mirroring the function_call.
OpenAIInputUserMessage<OpenAIMessage> | Regenerate | ToolResult<OpenAIToolResultPayload> | ToolResultError<OpenAIToolResultErrorPayload> | ToolApprovalResponse<OpenAIToolApprovalResponsePayload>
Every record-shape a client publishes on the ai-input wire. The SDK's well-known input variants, with the tool variants parameterised by the OpenAI payload shapes.
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.

OpenAIProjection{ messages: CodecMessage<OpenAIMessage>[] }
Per-run projection, carrying the { codecMessageId, message } pair list in publication order. The SDK does not inspect this shape. Use getMessages instead.

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

Example

Decode a single Ably message and fold the resulting events into a fresh projection. ReducerMeta.serial is required, because the reducer uses it as the high-water-mark for idempotency.

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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

const decoder = ResponsesCodec.createDecoder();
let projection = ResponsesCodec.init();

channel.subscribe((message) => {
  if (!message.serial) return; // live channel-subscribe messages always carry one
  const { inputs, outputs } = decoder.decode(message);
  for (const input of inputs) {
    projection = ResponsesCodec.fold(projection, input, { serial: message.serial });
  }
  for (const output of outputs) {
    projection = ResponsesCodec.fold(projection, output, { serial: message.serial });
  }
  render(ResponsesCodec.getMessages(projection).map((entry) => entry.message));
});
  • Conversation helpers: toResponsesInput and the correlation readers that drive the agent loop.
  • 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.