Agent presence
Your users see when the agent is thinking, streaming, idle, or offline. An agent reports its state on the conversation's channel and every client sees it in realtime.
Agent presence lets every client on a conversation see which agents are active and what they are doing. It uses Ably's Presence on the conversation's channel. This works for a single agent or a fleet of sub-agents, and can show whether the agent is streaming, thinking, idle, or offline.
How it works
The transport does not have a presence API of its own. You get the channel the transport configured from the client by name with no options, ably.channels.get(channelName), and use its presence object. Presence is part of the default channel modes, so it works on the transport's channel whatever channelModes you pass.
The agent enters presence with its initial status, updates the status as it moves through a response, and leaves when it finishes or shuts down. Every connected client receives those updates in realtime. The agent's Ably client needs a clientId, so clients can tell the agent apart in the presence set:
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
31
// Agent code. This runs on your own infrastructure, so it holds the API key.
import * as Ably from 'ably';
import OpenAI from 'openai';
import { createTransport } from '@ably/ai-transport';
import { openai } from '@ably/ai-transport/openai';
const ably = new Ably.Realtime({ key: process.env.ABLY_API_KEY, clientId: 'agent' });
const client = new OpenAI();
export async function POST(req) {
const { channelName, items, requestId } = await req.json();
const transport = createTransport({ client: ably, channelName, codec: openai });
const presence = ably.channels.get(channelName).presence;
await presence.enter({ status: 'thinking' });
try {
const stream = await client.responses.create(
{ model: 'gpt-5.5', input: [...(await loadConversation(channelName)), ...items], stream: true },
{ signal: req.signal },
);
await presence.update({ status: 'streaming' });
await transport.pipe(stream, {
signal: req.signal,
headers: { requestId },
});
} finally {
await presence.leave();
await transport.close();
}
return new Response(null, { status: 204 });
}Subscribe to agent status
On the client, subscribe to presence on the same channel to track the agent's state as it changes:
1
2
3
4
5
6
7
8
9
10
11
// Client code.
const presence = ably.channels.get(channelName).presence;
presence.subscribe((member) => {
if (member.clientId === 'agent') {
console.log(`Agent is ${member.data?.status}`);
}
});
const members = await presence.get();
const agent = members.find((m) => m.clientId === 'agent');You can put whatever your UI needs into presence data: a status, a progress percentage, or the name of the tool the agent is calling.
Combine presence with active responses
On the client, combine presence data with the response's own events for a more accurate status indicator. Presence tells you the agent's reported state.
With the OpenAI codec, a response.output_item.added event marks a response streaming, and a response.completed, response.incomplete, response.failed or error event marks it ended:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Client code.
const streaming = new Set();
const ended = ['response.completed', 'response.incomplete', 'response.failed', 'error'];
transport.subscribe(({ event, message }) => {
const requestId = message.extras?.headers?.requestId;
if (event?.type === 'response.output_item.added') streaming.add(requestId);
if (ended.includes(event?.type)) streaming.delete(requestId);
});
const members = await ably.channels.get(channelName).presence.get();
const agent = members.find((m) => m.clientId === 'agent');
const isIdle = agent?.data?.status === 'idle' && streaming.size === 0;
const isOffline = !agent;Your UI can use those values to show a typing indicator while the agent thinks, a streaming animation while the response arrives, and an offline badge when the agent disconnects.
React
TransportProvider wraps its children in an ably-js <ChannelProvider> for the conversation's channel, so ably-js's presence hooks (usePresence, usePresenceListener) work in any component inside it, without a <ChannelProvider> of your own. Read the agent's reported status from the presence set:
1
2
3
4
5
6
7
8
9
import { usePresenceListener } from 'ably/react';
function AgentStatus({ channelName }) {
const { presenceData } = usePresenceListener({ channelName });
const agent = presenceData.find((member) => member.clientId === 'agent');
if (!agent) return <span>Agent offline</span>;
return <span>Agent is {agent.data?.status}</span>;
}Edge cases and unhappy paths
- An agent that exits without calling
presence.leave(), such as a crashed process, is removed from presence after a timeout. Until the timeout fires, the agent is treated as present. Callleave()in afinallyblock, as the agent route above does. - A serverless agent that starts for one response and then stops should enter and leave presence for each response. A long-running agent can enter once and leave once when it shuts down.
- Presence updates are not ordered with the messages on the channel. A
streamingupdate can arrive slightly after the first part of the response. Use the response's own events for whether a response is streaming, and presence for the status the agent reports. - Each agent needs its own
clientId. Two agents with the sameclientIdcollide in the presence set. - A client needs the
subscribeorpresence-subscribecapability to receive presence updates, and the agent needs thepresencecapability to enter presence. Capability scoping is part of authentication.
FAQ
Does presence consume a message?
Presence enter, update, and leave each consume a message on the channel, billed at the current message rates.
Can clients enter presence too?
Yes. A client that enters presence shows up alongside agents in the presence set. Use the clientId to tell them apart.
How long does presence persist after a disconnect?
Until Ably's presence timeout fires, currently around 15 seconds. Active connections are not affected, because the timeout only applies to ungraceful disconnects.
What is the difference between presence and a streaming response?
Presence is the status the agent reports about itself. A response's own events, such as start and finish, show whether it is streaming. Both together produce a more accurate status.
Can I pause inference when no users are connected?
Yes. Subscribe to presence and check whether any clients other than agents are present. If none are, stop the model call by aborting its signal.
Related features
- Presence: the Ably Presence API used for agent status.
- Multi-device and fan-out: presence works across every connected device.