+
+| 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..33f0116b4e 100644
--- a/src/pages/docs/ai-transport/features/agent-presence.mdx
+++ b/src/pages/docs/ai-transport/features/agent-presence.mdx
@@ -1,23 +1,23 @@
---
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."
+meta_description: "See which clients and agents are connected to an AI Transport session with Ably Presence, exposed directly on the session."
+meta_keywords: "agent presence, presence API, online status, AI Transport, Ably"
+intro: "AI Transport session channels carry Ably Presence. Use it to see which clients and agents are connected to a session 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.
-
-
+An AI Transport session channel is an ordinary Ably channel, so it carries Ably [Presence](/docs/presence-occupancy/presence) like any other channel. Use presence to see which participants — agents or users — are connected to a session in real time. Both [`ClientSession`](/docs/ai-transport/api/javascript/core/client-session) and [`AgentSession`](/docs/ai-transport/api/javascript/core/agent-session) expose the channel's presence object directly as `session.presence`, so you don't need to resolve the channel separately.
-## How it works
+## Presence on a session
+
+`session.presence` returns the same `Ably.RealtimePresence` instance the session's channel exposes. The session adds no semantics of its own: `enter`, `update`, `leave`, `get`, and `subscribe` behave exactly as they do on a raw Ably channel. Presence operations implicitly attach the channel, so you can call them without first awaiting `connect()`.
-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.
+An agent enters presence so every connected client knows it is online, and leaves when it shuts down:
```javascript
@@ -27,9 +27,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 on the session channel so every subscriber sees the agent online.
+ await session.presence.enter();
await run.start();
await run.loadConversation();
@@ -40,59 +39,72 @@ app.post('/api/chat', async (req, res) => {
abortSignal: run.abortSignal,
});
- await channel.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 });
});
```
-## Subscribe to agent status
+## See who is connected
-On the client, subscribe to presence events to track the agent's current state:
+On the client, subscribe to presence events to track who is connected as participants come and go:
```javascript
-const channel = ably.channels.get(sessionChannelName);
+const session = createClientSession({ client: ably, channelName, codec: UIMessageCodec });
-channel.presence.subscribe((member) => {
- if (member.clientId === 'agent') {
- console.log(`Agent is ${member.data.status}`);
- }
+session.presence.subscribe((member) => {
+ console.log(member.clientId, member.action); // 'enter' | 'leave' | ...
});
-const members = await channel.presence.get();
-const agent = members.find((m) => m.clientId === 'agent');
+const members = await session.presence.get();
```
-## 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:
+Presence is symmetric — agents and users enter the same presence set, distinguished by `clientId`. If a participant's role isn't clear from its `clientId`, pass presence data on `enter` to label it:
```javascript
-const { session } = useClientSession();
-const agentStatus = useAgentPresence(channel); // your custom hook
-
-const isStreaming = session.view.runs().some((r) => r.status === 'active' && r.clientId === 'agent');
-const isIdle = agentStatus === 'idle' && !isStreaming;
-const isOffline = agentStatus === null;
+await session.presence.enter({ role: '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 channel hooks — [`usePresence`, `usePresenceListener`](/docs/getting-started/react#step-3), and `useChannel` — work for any descendant without wrapping the subtree in your own ``. Pass the same `channelName` you gave the provider:
+
+
+```javascript
+import { usePresence, usePresenceListener } from 'ably/react';
+
+function OnlineList() {
+ // Enter presence on the session channel.
+ usePresence({ channelName: 'ai:demo' });
+
+ // Read the current presence set.
+ const { presenceData } = usePresenceListener({ channelName: 'ai:demo' });
+
+ return (
+
+ {presenceData.map((member) => (
+
{member.clientId}
+ ))}
+
+ );
+}
+```
+
## 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.
-- A client without `presence` capability cannot subscribe to updates. Capability scoping is part of [authentication](/docs/ai-transport/concepts/authentication).
+- A participant that disconnects without calling `leave()` (for example, a crashed agent process) is removed from presence automatically once Ably's presence timeout fires. Wire a graceful shutdown that calls `leave()` for the cleanest result.
+- A serverless agent that comes up for one turn and tears down should enter and leave presence per turn; a long-running agent enters once and leaves on shutdown.
+- Multiple agents need a unique `clientId` each. Two participants with the same `clientId` collide in the presence set.
+- A client without `presence` capability cannot enter or subscribe. Capability scoping is part of [authentication](/docs/ai-transport/concepts/authentication).
## FAQ
@@ -100,23 +112,21 @@ This is enough information for the UI to show a typing indicator while the agent
Presence enter, update, and leave each consume a message on the channel. See [the platform pricing](/docs/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.
-
### 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?
+Until Ably's presence timeout fires (currently around 15 seconds). Active connections are unaffected; this is for ungraceful disconnects.
+
+### How do I tell whether the agent is actively responding?
-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.
+Read it from the conversation, not presence. `session.view.runs()` reflects run lifecycle observed from the channel — which runs are active, suspended, or terminal — without relying on a participant to self-report. Presence tells you who is *connected*; runs tell you what is *happening*.
### 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.
+- [Presence](/docs/presence-occupancy/presence): the Ably Presence API.
+- [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.