Agent presence

Your users see when the agent is thinking, streaming, idle, or offline. An agent self-reports its state on the session and every client sees it in real time.

Agent presence gives session participants a real-time view of which agents are active and what they are doing. Agent presence uses Ably's native Presence through the session.presence object on the AI Transport session. This works for a single orchestrator agent or a fleet of sub-agents, and conveys whether the agent is streaming, thinking, idle, or offline.

Diagram showing presence-aware agent status updates

How it works

Both ClientSession and AgentSession expose presence directly as session.presence, with the standard enter(), update(), leave(), get(), and subscribe() operations. Presence operations attach the session's channel for you, so you can call them without first awaiting connect().

The agent enters presence with its initial status, then updates that status as it moves through a turn (receiving a message, thinking, streaming, finishing) and leaves when it shuts down. Every connected client receives those updates in real time.

JavaScript

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

app.post('/api/chat', async (req, res) => {
  const invocation = Invocation.fromJSON(await req.json());
  const session = createAgentSession({ client: ably, channelName: invocation.sessionName, codec: UIMessageCodec });
  await session.connect();
  const run = session.createRun(invocation, { signal: req.signal });

  // Enter presence so every connected client sees what the agent is doing.
  await session.presence.enter({ status: 'thinking' });

  await run.start();
  await run.loadConversation();

  const result = streamText({
    model: openai('gpt-4o'),
    messages: run.messages,
    abortSignal: run.abortSignal,
  });

  await session.presence.update({ status: 'streaming' });
  const { reason } = await run.pipe(result.toUIMessageStream());
  await run.end({ reason });

  await session.presence.leave();
  await session.close();
  res.json({ ok: true });
});

Subscribe to agent status

On the client, subscribe to presence events to track the agent's current state as it changes:

JavaScript

1

2

3

4

5

6

7

8

9

10

const session = createClientSession({ client: ably, channelName, codec: UIMessageCodec });

session.presence.subscribe((member) => {
  if (member.clientId === 'agent') {
    console.log(`Agent is ${member.data.status}`);
  }
});

const members = await session.presence.get();
const agent = members.find((m) => m.clientId === 'agent');

You can put whatever your UI needs into presence data: a coarse status, a progress percentage, the name of the tool the agent is currently calling. Presence carries the agent's self-report; the conversation itself carries the run lifecycle.

Combine presence with active runs

For richer status indicators, combine presence data with the active runs on the view. Presence tells you the agent's self-reported state; session.view.runs() tells you which runs are actually in progress:

JavaScript

1

2

3

4

5

6

7

const { session } = useClientSession();
const { presenceData } = usePresenceListener({ channelName: 'ai:demo' });
const agent = presenceData.find((m) => m.clientId === 'agent');

const isStreaming = session.view.runs().some((r) => r.status === 'active' && r.clientId === 'agent');
const isIdle = agent?.data?.status === 'idle' && !isStreaming;
const isOffline = !agent;

This is enough information for the UI to show a typing indicator while the agent thinks, a streaming animation while tokens arrive, and an offline badge when the agent disconnects.

React

ClientSessionProvider (and ChatTransportProvider, which wraps it) renders an ably-js <ChannelProvider> for the session's channel, so ably-js's presence hooks (usePresence, usePresenceListener) work for any descendant without wrapping the subtree in your own <ChannelProvider>. Read the agent's reported status straight from the presence set:

JavaScript

1

2

3

4

5

6

7

8

9

import { usePresenceListener } from 'ably/react';

function AgentStatus() {
  const { presenceData } = usePresenceListener({ channelName: 'ai:demo' });
  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() (for example, a crashed process) is automatically removed from presence after a timeout. The agent is treated as present until the timeout fires. Wire a graceful shutdown that calls leave() for the best user experience.
  • A serverless agent that comes up for one turn and tears down should enter and leave presence per turn; entering once and leaving once at the end is fine for a long-running agent.
  • Presence updates do not guarantee strict ordering with channel messages. A streaming presence update sometimes arrives slightly after the first token. Drive the UI off session.view.runs() for run-level state (active, suspended, terminal) and use presence for higher-level status the agent self-reports.
  • Multi-agent setups need a unique clientId per agent. Two agents with the same clientId collide in the presence set.
  • A client without presence capability cannot subscribe to updates. Capability scoping is part of authentication.

FAQ

Does presence cost a message?

Presence enter, update, and leave each consume a message on the channel. See the platform pricing for current rates.

Can clients enter presence too?

Yes. Presence is symmetric. A client that enters presence shows up alongside agents in the presence set. Use the clientId to distinguish them.

How long does presence persist after a disconnect?

Until Ably's presence timeout fires (currently around 15 seconds). Active connections are not affected; this is for ungraceful disconnects.

What is the difference between presence and the view's active runs?

Presence is self-reported by the agent. session.view.runs() is observable from the channel by inspecting run lifecycle events. Presence reports intent; active runs report fact. Both together produce richer status.

Can I pause inference when no users are connected?

Yes. Subscribe to presence and check whether any non-agent participants are present. If none, end the run or short-circuit the LLM call. This is one of the cost-saving patterns presence enables.