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..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. - -![Diagram showing presence-aware agent status updates](../../../../images/content/diagrams/ait-presence-aware.png) +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.