Conversation helpers

Four functions in @ably/ai-transport/openai read a conversation of OpenAIMessage objects. toResponsesInput turns one into model input, and the three correlation readers tell an agent what its tool calls are still waiting on. All four are pure functions, so you pass them the messages from a drained run.view.

A run splits its work across messages, so a function_call and its function_call_output land on separate messages, and a gated call's approval decision folds onto the message holding the call. These readers pair those pieces up by call_id, in whatever order they arrived, so an agent decides what to do next from the conversation alone.

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

import {
  toResponsesInput,
  unansweredCalls,
  approvedUnexecutedCalls,
} from '@ably/ai-transport/openai';

// Agent, resuming a suspended run.
while (run.view.hasOlder()) await run.view.loadOlder();
const messages = run.view.getMessages().map(({ message }) => message);

// Every open call needs an answer before the model will accept the conversation.
if (unansweredCalls(messages).length > 0) return;

// An approved call has no output yet, so run it before the next model turn.
const approved = approvedUnexecutedCalls(messages);

const input = toResponsesInput(messages);

Build model input

toResponsesInput(messages: OpenAIMessage[]): Responses.ResponseInputItem[]

Flatten a conversation into the input array for a Responses API call. Each stored message already holds valid Responses input items, so it needs no conversion.

Every item the codec stores is a ResponseInputItem, so adding an item type the model cannot accept breaks the build at this function instead of failing at request time.

toResponsesInput reads only message.items and never toolCallStates, so approval and client-execution state cannot reach the model.

Parameters

messagesrequiredOpenAIMessage[]
The conversation to flatten, usually run.view.getMessages().map(({ message }) => message) after draining history.

Returns

Responses.ResponseInputItem[]. Pass it straight to openai.responses.create({ input, ... }).

Correlation readers

The three readers all take the same OpenAIMessage[] and answer a different question about the run's tool calls. An agent resuming a run reads unansweredCalls to decide whether it can continue at all, and approvedUnexecutedCalls to find the work it owes before the next model turn.

resolvedCallIds

resolvedCallIds(messages: OpenAIMessage[]): Set<string>

The call_id of every function_call_output in the conversation. A resolved call has its output folded in, so a renderer shows that output attached to the call, and the agent's loop skips it.

unansweredCalls

unansweredCalls(messages: OpenAIMessage[]): Responses.ResponseFunctionToolCall[]

The function calls that still owe the model an answer, in message and item order.

The model input must carry a matching output for every open function_call, so resuming while any call is unanswered makes the provider reject the request. A turn that emits two gated calls therefore needs both answers before either one wakes the agent.

Has a function_call_outputThe output has folded in, which is what resolvedCallIds reports.
Answered
Approved, no output yetThe agent runs it server-side on resume, so the output exists by the time the model sees the conversation. approvedUnexecutedCalls returns these.
Answered
DeniedThe reducer resolves a denial with a rejection function_call_output.
Answered
Pending a decisionNobody has approved or denied it yet.
Unanswered
Client-executed, result not arrivedThe browser has not published its result.
Unanswered

A client's own resolution is wire-only, because the transport skips the optimistic fold for an input targeting an existing message. A client deciding whether it answered the run's last open call should track the call_ids it has answered rather than waiting for them to appear in its view.

approvedUnexecutedCalls

approvedUnexecutedCalls(messages: OpenAIMessage[]): Responses.ResponseFunctionToolCall[]

The gated calls the user approved that the agent has not run yet, in message and item order. Each is a function_call whose toolCallStates[call_id].approval is 'approved' with no function_call_output present.

An approval is a decision and carries no output, so on resume the agent runs each of these server-side, publishes the outputs, and feeds them back before the next model turn. Skipping this step leaves a dangling function_call that the provider rejects.

  • ResponsesCodec: the codec, its tool payloads, and the OpenAIMessage shape these functions read.
  • OpenAI Responses: the tool and approval flow these functions support, end to end.