From b422bb700fb3f917d022726a7e301ad7f5265e61 Mon Sep 17 00:00:00 2001 From: owenpearson Date: Wed, 10 Jun 2026 17:27:16 +0100 Subject: [PATCH] add docs for ait presence --- .../api/javascript/core/agent-session.mdx | 10 +++ .../api/javascript/core/client-session.mdx | 6 +- .../api/javascript/core/codec.mdx | 6 +- .../api/javascript/vercel/chat-transport.mdx | 4 +- .../ai-transport/api/react/core/providers.mdx | 5 +- .../react/vercel/chat-transport-provider.mdx | 5 +- .../docs/ai-transport/concepts/sessions.mdx | 2 + .../ai-transport/features/agent-presence.mdx | 69 +++++++++++++------ 8 files changed, 70 insertions(+), 37 deletions(-) diff --git a/src/pages/docs/ai-transport/api/javascript/core/agent-session.mdx b/src/pages/docs/ai-transport/api/javascript/core/agent-session.mdx index bb3bf5830c..070a99cd74 100644 --- a/src/pages/docs/ai-transport/api/javascript/core/agent-session.mdx +++ b/src/pages/docs/ai-transport/api/javascript/core/agent-session.mdx @@ -33,6 +33,16 @@ await run.start(); ``` +## Properties + + + +| 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). | | +| options | optional | Per-encoder defaults (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). |
| +| options | optional | Per-write overrides (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. ![Diagram showing presence-aware agent status updates](../../../../images/content/diagrams/ait-presence-aware.png) ## 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.