+
+| Property | Description | Type |
+| --- | --- | --- |
+| presence | The Ably presence object for the session's channel. Use it to see which clients are connected — for example, to detect whether the requesting user is still online (`enter`, `leave`, `get`, `subscribe`). The session adds no semantics — it is the same instance the channel exposes — and presence operations implicitly attach, so they work without first awaiting [`connect()`](#connect). | `Ably.RealtimePresence` |
+
+
+
## Create an agent session {`function createAgentSession(options: AgentSessionOptions): AgentSession`}
diff --git a/src/pages/docs/ai-transport/api/javascript/core/client-session.mdx b/src/pages/docs/ai-transport/api/javascript/core/client-session.mdx
index bb44d2b099..b4108ad6bc 100644
--- a/src/pages/docs/ai-transport/api/javascript/core/client-session.mdx
+++ b/src/pages/docs/ai-transport/api/javascript/core/client-session.mdx
@@ -37,6 +37,7 @@ await session.connect();
| --- | --- | --- |
| tree | The complete conversation tree. Holds every known Run node and emits events on any change. Use `view` in most cases; reach for `tree` for low-level inspection. |
|
| view | The default paginated, branch-aware view for rendering. Events scope to the visible messages. |
|
+| presence | The Ably presence object for the session's channel. Use it to see which clients are connected (`enter`, `leave`, `get`, `subscribe`). The session adds no semantics — it is the same instance the channel exposes — and presence operations implicitly attach, so they work without first awaiting [`connect()`](#connect). | `Ably.RealtimePresence` |
@@ -89,7 +90,6 @@ const session = createClientSession({
client: ably,
channelName: 'conversation-42',
codec: UIMessageCodec,
- clientId: 'user-abc',
});
```
@@ -100,10 +100,9 @@ const session = createClientSession({
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
-| client | required | The Ably Realtime client. The caller owns its lifecycle; `session.close()` does not close the client. | `Ably.Realtime` |
+| client | required | The Ably Realtime client. The caller owns its lifecycle; `session.close()` does not close the client. The session's identity is read from this client's `auth.clientId` at publish time, stamped on the wire as the run-owner / input-owner id so other clients can attribute messages. A connection with no concrete clientId (anonymous, or a wildcard `*` token) publishes without one. | `Ably.Realtime` |
| channelName | required | The channel to subscribe to and publish cancel signals on. The session owns this channel; do not also resolve it elsewhere with conflicting options. | String |
| codec | required | The codec used to encode and decode events and messages. | `Codec` |
-| clientId | optional | The client's identity, used as the Ably publisher `clientId` on everything this session publishes. Surfaces on the wire as the run-owner / input-owner id so other clients can attribute messages. | String |
| messages | optional | Initial messages to seed the conversation tree with. Forms a linear chain. | `TMessage[]` |
| logger | optional | Logger instance for diagnostic output. | `Logger` |
@@ -266,7 +265,6 @@ const session = createClientSession({
client: ably,
channelName: 'conversation-42',
codec: UIMessageCodec,
- clientId: 'user-abc',
});
await session.connect();
diff --git a/src/pages/docs/ai-transport/api/javascript/core/codec.mdx b/src/pages/docs/ai-transport/api/javascript/core/codec.mdx
index deef39b80d..d895c12e65 100644
--- a/src/pages/docs/ai-transport/api/javascript/core/codec.mdx
+++ b/src/pages/docs/ai-transport/api/javascript/core/codec.mdx
@@ -92,7 +92,7 @@ Build a stateful encoder bound to a channel. The encoder owns the message-append
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| channel | required | The channel writer to publish through. An `Ably.RealtimeChannel` satisfies this directly. | `ChannelWriter` |
-| options | optional | Per-encoder defaults (clientId, extras, messageId, onMessage hook). |
|
@@ -100,7 +100,6 @@ Build a stateful encoder bound to a channel. The encoder owns the message-append
| Property | Description | Type |
| --- | --- | --- |
-| clientId | Default clientId for all writes. | String |
| extras | Default extras merged into every Ably message. |
|
| onMessage | Hook called before each Ably message is published. Mutate the message in place to add transport-level headers under `extras.ai`. | `(message: Ably.Message) => void` |
| messageId | Default `codec-message-id` for messages where the event payload doesn't supply one. | String |
@@ -150,7 +149,6 @@ A stateful encoder for a single channel. Two publish methods enforce direction a
| Property | Description | Type |
| --- | --- | --- |
-| clientId | Override the default clientId for this write. | String |
| extras | Override the default extras for this write. |
|
| messageId | Message identity for projection routing. Stamped as `codec-message-id`. | String |
@@ -169,7 +167,7 @@ Encode and publish a single client input on the `ai-input` wire. Per-write overr
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
| input | required | The input event to encode and publish. | `TInput` |
-| options | optional | Per-write overrides (clientId, extras, messageId). |
|
diff --git a/src/pages/docs/ai-transport/api/javascript/vercel/chat-transport.mdx b/src/pages/docs/ai-transport/api/javascript/vercel/chat-transport.mdx
index 4e621b7f72..e92ef56454 100644
--- a/src/pages/docs/ai-transport/api/javascript/vercel/chat-transport.mdx
+++ b/src/pages/docs/ai-transport/api/javascript/vercel/chat-transport.mdx
@@ -45,7 +45,6 @@ const ably = new Ably.Realtime({ authUrl: '/api/auth/token' });
const session = createClientSession({
client: ably,
channelName: 'conversation-42',
- clientId: 'user-abc',
});
```
@@ -56,9 +55,8 @@ const session = createClientSession({
| Parameter | Required | Description | Type |
| --- | --- | --- | --- |
-| client | required | The Ably Realtime client. The caller owns its lifecycle. | `Ably.Realtime` |
+| client | required | The Ably Realtime client. The caller owns its lifecycle. The session's identity is read from this client's `auth.clientId` at publish time and stamped on everything it publishes. | `Ably.Realtime` |
| channelName | required | The channel to subscribe to and publish cancel signals on. | String |
-| clientId | optional | The client's identity, used as the Ably publisher `clientId` on everything this session publishes. | String |
| messages | optional | Initial messages to seed the conversation tree with. | `UIMessage[]` |
| logger | optional | Logger instance for diagnostic output. | `Logger` |
diff --git a/src/pages/docs/ai-transport/api/react/core/providers.mdx b/src/pages/docs/ai-transport/api/react/core/providers.mdx
index 9afb5a0483..d039d9bc10 100644
--- a/src/pages/docs/ai-transport/api/react/core/providers.mdx
+++ b/src/pages/docs/ai-transport/api/react/core/providers.mdx
@@ -38,9 +38,11 @@ The provider reads the Ably Realtime client from the surrounding `
If `createClientSession` throws, the error is stored on the slot alongside an undefined session. `useClientSession` surfaces it as [`sessionError`](/docs/ai-transport/api/react/core/use-client-session#returns) so the component tree does not crash.
+The provider also renders an ably-js `` for the session's channel, so ably-js's channel hooks — `usePresence`, `usePresenceListener`, `useChannel` — work for any descendant without wrapping the subtree in your own ``. Pass the same `channelName` you gave the provider. See [Agent presence](/docs/ai-transport/features/agent-presence#react).
+
### Props
-`ClientSessionProviderProps` extends all [`ClientSessionOptions`](/docs/ai-transport/api/javascript/core/client-session#constructor-params) except `client` (which is read from ``).
+`ClientSessionProviderProps` extends all [`ClientSessionOptions`](/docs/ai-transport/api/javascript/core/client-session#constructor-params) except `client` (which is read from ``). The session's identity is taken from that client's `auth.clientId` (set via the Ably token or `ClientOptions.clientId`) and stamped on everything it publishes.
@@ -48,7 +50,6 @@ If `createClientSession` throws, the error is stored on the slot alongside an un
| --- | --- | --- |
| channelName | The channel the session subscribes to. Used as the registry key for nested providers. | String |
| codec | The codec used to encode and decode events and messages. | `Codec` |
-| clientId | The client's identity, used as the Ably publisher `clientId` for everything the session publishes. | String |
| messages | Initial messages to seed the conversation tree with. | `TMessage[]` |
| logger | Logger instance for diagnostic output. | `Logger` |
| children | Descendant components that consume the session via hooks. | `ReactNode` |
diff --git a/src/pages/docs/ai-transport/api/react/vercel/chat-transport-provider.mdx b/src/pages/docs/ai-transport/api/react/vercel/chat-transport-provider.mdx
index 2d9c5cab5f..4d83219edf 100644
--- a/src/pages/docs/ai-transport/api/react/vercel/chat-transport-provider.mdx
+++ b/src/pages/docs/ai-transport/api/react/vercel/chat-transport-provider.mdx
@@ -35,7 +35,7 @@ function App() {
## Props
-`ChatTransportProviderProps` extends [`ClientSessionProviderProps`](/docs/ai-transport/api/react/core/providers#client-session-provider-props) with the `codec` field omitted (always `UIMessageCodec`) and adds four transport-owned options for the agent-invocation POST.
+`ChatTransportProviderProps` extends [`ClientSessionProviderProps`](/docs/ai-transport/api/react/core/providers#client-session-provider-props) with the `codec` field omitted (always `UIMessageCodec`) and adds four transport-owned options for the agent-invocation POST. As with `ClientSessionProvider`, the session's identity is taken from the `` client's `auth.clientId` (set via the Ably token or `ClientOptions.clientId`).
@@ -46,7 +46,6 @@ function App() {
| credentials | Fetch credentials mode for the invocation POST. Set to `'include'` for cookie-based cross-origin auth. | `RequestCredentials` |
| fetch | Custom fetch implementation for the invocation POST. Defaults to `globalThis.fetch`. | `typeof globalThis.fetch` |
| chatOptions | Hooks for customising chat request construction (for example `prepareSendMessagesRequest`). Must be stable across renders; wrap in `useMemo` or define outside the component. A new object reference recreates the `ChatTransport`. |
|
-| clientId | The client's identity, used as the Ably publisher `clientId` on everything the session publishes. | String |
| messages | Initial messages to seed the conversation tree with. | `UIMessage[]` |
| logger | Logger instance for diagnostic output. | `Logger` |
| children | Descendant components that consume the chat transport via [`useChatTransport`](/docs/ai-transport/api/react/vercel/use-chat-transport). | `ReactNode` |
@@ -67,6 +66,8 @@ function App() {
The underlying `ClientSession` is created once on first render via `useRef`, and `connect()` runs from a `useEffect`. The session is closed when the provider truly unmounts.
+Because it wraps [`ClientSessionProvider`](/docs/ai-transport/api/react/core/providers#client-session-provider), it also renders an ably-js `` for the channel, so ably-js's `usePresence`, `usePresenceListener`, and `useChannel` hooks work in the subtree. See [Agent presence](/docs/ai-transport/features/agent-presence#react).
+
The `ChatTransport` itself is not closed on unmount: its `close()` delegates to `ClientSession.close()`, which the inner `ClientSessionProvider` already calls. Auto-closing here would double-close in React Strict Mode.
If `createClientSession` throws, the error surfaces via [`useClientSession.sessionError`](/docs/ai-transport/api/react/core/use-client-session#returns) and [`useChatTransport.chatTransportError`](/docs/ai-transport/api/react/vercel/use-chat-transport). The component tree does not crash.
diff --git a/src/pages/docs/ai-transport/concepts/sessions.mdx b/src/pages/docs/ai-transport/concepts/sessions.mdx
index 9de6fd8288..a76ba707f0 100644
--- a/src/pages/docs/ai-transport/concepts/sessions.mdx
+++ b/src/pages/docs/ai-transport/concepts/sessions.mdx
@@ -81,6 +81,8 @@ Clients are resilient to disconnection. A client that drops its connection loses
New participants join at any time. A second client attaching to the channel hydrates the full session from history and receives live updates going forward. No handshake or coordination is required between participants.
+To see which participants are currently connected, use presence: the session channel carries Ably Presence, exposed directly as `session.presence`. See [Agent presence](/docs/ai-transport/features/agent-presence).
+
## Support persistence models
The channel serves two distinct roles: live delivery and historical persistence. It always serves as the live delivery layer. Whether it also serves as the historical persistence layer depends on the configuration.
diff --git a/src/pages/docs/ai-transport/features/agent-presence.mdx b/src/pages/docs/ai-transport/features/agent-presence.mdx
index 05194007cc..a092bf7322 100644
--- a/src/pages/docs/ai-transport/features/agent-presence.mdx
+++ b/src/pages/docs/ai-transport/features/agent-presence.mdx
@@ -2,22 +2,24 @@
title: "Agent presence"
meta_description: "Show agent status in your AI application with Ably Presence. Display streaming, thinking, idle, and offline states in real time."
meta_keywords: "agent presence, AI status, presence API, agent state, AI Transport, Ably"
-intro: "Your users see when the agent is thinking, streaming, idle, or offline. AI Transport channels carry Ably Presence, so an agent self-reports its state and every client sees it in real time."
+intro: "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."
redirect_from:
- /docs/ai-transport/sessions-identity/online-status
---
-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](/docs/presence-occupancy/presence) API on the AI Transport session channel. This works for a single orchestrator agent or a fleet of sub-agents, and conveys whether the agent is streaming, thinking, idle, or offline.
+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](/docs/presence-occupancy/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.

## How it works
-The agent enters presence on the AI Transport session channel with status data. As the agent moves through its turn lifecycle (receiving a message, thinking, streaming, finishing), it updates its presence data. Every connected client receives these updates in real time.
+Both [`ClientSession`](/docs/ai-transport/api/javascript/core/client-session) and [`AgentSession`](/docs/ai-transport/api/javascript/core/agent-session) 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
@@ -27,9 +29,8 @@ app.post('/api/chat', async (req, res) => {
await session.connect();
const run = session.createRun(invocation, { signal: req.signal });
- // Enter presence on this invocation's channel so every subscriber sees the agent.
- const channel = ably.channels.get(invocation.sessionName);
- await channel.presence.enter({ status: 'thinking' });
+ // Enter presence so every connected client sees what the agent is doing.
+ await session.presence.enter({ status: 'thinking' });
await run.start();
await run.loadConversation();
@@ -40,11 +41,12 @@ app.post('/api/chat', async (req, res) => {
abortSignal: run.abortSignal,
});
- await channel.presence.update({ status: 'streaming' });
+ await session.presence.update({ status: 'streaming' });
const { reason } = await run.pipe(result.toUIMessageStream());
await run.end(reason);
- await channel.presence.leave();
- session.close();
+
+ await session.presence.leave();
+ await session.close();
res.json({ ok: true });
});
```
@@ -52,46 +54,67 @@ app.post('/api/chat', async (req, res) => {
## Subscribe to agent status
-On the client, subscribe to presence events to track the agent's current state:
+On the client, subscribe to presence events to track the agent's current state as it changes:
```javascript
-const channel = ably.channels.get(sessionChannelName);
+const session = createClientSession({ client: ably, channelName, codec: UIMessageCodec });
-channel.presence.subscribe((member) => {
+session.presence.subscribe((member) => {
if (member.clientId === 'agent') {
console.log(`Agent is ${member.data.status}`);
}
});
-const members = await channel.presence.get();
+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 in progress:
+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
const { session } = useClientSession();
-const agentStatus = useAgentPresence(channel); // your custom hook
+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 = agentStatus === 'idle' && !isStreaming;
-const isOffline = agentStatus === null;
+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 `` for the session's channel, so ably-js's presence hooks ([`usePresence`, `usePresenceListener`](/docs/getting-started/react#step-3)) work for any descendant without wrapping the subtree in your own ``. Read the agent's reported status straight from the presence set:
+
+
+```javascript
+import { usePresenceListener } from 'ably/react';
+
+function AgentStatus() {
+ const { presenceData } = usePresenceListener({ channelName: 'ai:demo' });
+ const agent = presenceData.find((member) => member.clientId === 'agent');
+
+ if (!agent) return Agent offline;
+ return Agent is {agent.data?.status};
+}
+```
+
+
## 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 unique `clientId` per agent. Two agents with the same `clientId` collide in the presence set.
+- 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](/docs/ai-transport/concepts/authentication).
## FAQ
@@ -102,9 +125,10 @@ Presence enter, update, and leave each consume a message on the channel. See [th
### 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.
+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?
@@ -113,10 +137,11 @@ Presence is self-reported by the agent. `session.view.runs()` is observable from
### 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 turn or short-circuit the LLM call. This is one of the cost-saving patterns presence enables.
+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.
## Related features
- [Presence](/docs/presence-occupancy/presence): the Ably Presence API used for agent status.
-- [Concurrent turns](/docs/ai-transport/features/concurrent-turns): tracking active turns across clients.
+- [Sessions](/docs/ai-transport/concepts/sessions): `session.presence` on the client and agent sessions.
+- [Concurrent turns](/docs/ai-transport/features/concurrent-turns): tracking active runs across clients.
- [Multi-device sessions](/docs/ai-transport/features/multi-device): presence works across every connected device.