Skip to main content

agent_client_protocol/concepts/
sessions.rs

1//! Creating and managing sessions for multi-turn conversations.
2//!
3//! A **session** represents a multi-turn conversation with an agent. Within a
4//! session, you can send prompts, receive responses, and the agent maintains
5//! context across turns.
6//!
7//! The examples below use the stable protocol v1 `v1::SessionBuilder` and
8//! `v1::ActiveSession`. With the `unstable_protocol_v2` feature, callbacks created
9//! through `Client.v2()` receive `v2::ConnectionTo` and its `build_session*`,
10//! `v2::SessionBuilder`, `resume_session*`, `v2::ResumeSessionBuilder`, and
11//! command-only `v2::ActiveSession` APIs. With `unstable_session_fork`, it also exposes
12//! `fork_session*` and `v2::ForkSessionBuilder`. The v2 resume and fork helpers
13//! return builders and do not publish their requests until `start_session` or
14//! `on_proxy_session_start` is called. V2 prompt responses acknowledge
15//! acceptance independently; receive session-wide updates and interactive
16//! requests through typed connection handlers.
17//!
18//! # Creating a Session
19//!
20//! Use the session builder to create a new session. Every setup method hands
21//! back a [`SessionSetup`]: the session plus the complete response of the
22//! request that set it up:
23//!
24//! ```
25//! # use agent_client_protocol::{Client, Agent, ConnectTo};
26//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
27//! # Client.v1().connect_with(transport, async |cx| {
28//! cx.build_session_cwd()?          // Use current working directory
29//!     .block_task()                // Mark as blocking
30//!     .run_until(async |setup| {
31//!         // Use `setup.session` and `setup.response` here
32//!         Ok(())
33//!     })
34//!     .await?;
35//! # Ok(())
36//! # }).await?;
37//! # Ok(())
38//! # }
39//! ```
40//!
41//! Or specify a custom working directory:
42//!
43//! ```
44//! # use agent_client_protocol::{Client, Agent, ConnectTo};
45//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
46//! # Client.v1().connect_with(transport, async |cx| {
47//! cx.build_session("/path/to/project")
48//!     .block_task()
49//!     .run_until(async |session| { Ok(()) })
50//!     .await?;
51//! # Ok(())
52//! # }).await?;
53//! # Ok(())
54//! # }
55//! ```
56//!
57//! # Restoring a Session
58//!
59//! Stable protocol v1 direct clients can turn `session/load` and
60//! `session/resume` into a [`SessionSetup`] without manually installing session
61//! handlers. It contains both the [`ActiveSession`] and the complete
62//! operation-specific response:
63//!
64//! ```no_run
65//! # use agent_client_protocol::{Client, ConnectTo};
66//! use agent_client_protocol::v1::SessionSetup;
67//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
68//! # Client.v1().connect_with(transport, async |cx| {
69//! let SessionSetup {
70//!     mut session,
71//!     response: load_response,
72//!     ..
73//! } = cx
74//!     .load_session("session-1", "/path/to/project")
75//!     .block_task()
76//!     .start_session()
77//!     .await?;
78//! println!("load response: {load_response:?}");
79//! session.send_prompt("Continue where we left off")?;
80//! # Ok(())
81//! # }).await?;
82//! # Ok(())
83//! # }
84//! ```
85//!
86//! Use `load_session` only when the agent advertises the top-level
87//! `loadSession` capability. Use `resume_session` to continue without replay
88//! when the agent advertises `sessionCapabilities.resume`.
89//! The matching `load_session_from` and `resume_session_from` helpers preserve
90//! requests assembled elsewhere. Non-blocking callers can use
91//! `on_session_start` instead of `block_task().start_session()`.
92//!
93//! For `session/load`, the SDK acknowledges its local route before publishing
94//! the request, so history updates sent before the load response remain queued
95//! on the returned session. An error removes the provisional route before
96//! later traffic is dispatched. Dropping an in-flight blocking start
97//! immediately deactivates the provisional route and triggers the standard
98//! [`SentRequest`](crate::SentRequest) drop-time cancellation behavior.
99//!
100//! # Sending Prompts
101//!
102//! Inside `run_until`, the [`SessionSetup`] holds an [`ActiveSession`] that
103//! lets you interact with the agent:
104//!
105//! ```
106//! # use agent_client_protocol::{Client, Agent, ConnectTo};
107//! use agent_client_protocol::v1::SessionSetup;
108//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
109//! # Client.v1().connect_with(transport, async |cx| {
110//! # cx.build_session_cwd()?.block_task()
111//! .run_until(async |SessionSetup { mut session, .. }| {
112//!     // Send a prompt
113//!     session.send_prompt("What is 2 + 2?")?;
114//!
115//!     // Read the complete response as a string
116//!     let response = session.read_to_string().await?;
117//!     println!("{}", response);
118//!
119//!     // Send another prompt in the same session
120//!     session.send_prompt("And what is 3 + 3?")?;
121//!     let response = session.read_to_string().await?;
122//!
123//!     Ok(())
124//! })
125//! # .await?;
126//! # Ok(())
127//! # }).await?;
128//! # Ok(())
129//! # }
130//! ```
131//!
132//! The `session/prompt` response arrives on the session's update stream as
133//! [`SessionMessage::PromptResponse`](crate::v1::SessionMessage::PromptResponse),
134//! after the updates the agent sent for that turn. An error response arrives
135//! there too, and `read_to_string` returns it. It does not close the
136//! connection. Use `read_update` to see each
137//! [`SessionMessage`](crate::v1::SessionMessage), including the stop reason.
138//!
139//! # Adding MCP Servers
140//!
141//! You can attach MCP (Model Context Protocol) servers to a session to provide
142//! tools to the agent:
143//!
144//! MCP attachment requires the `unstable_mcp_over_acp` feature. Standalone MCP
145//! servers remain available without it. Draft protocol v2 per-session
146//! attachment uses `v2::SessionBuilder::with_mcp_server` for new sessions or
147//! `v2::ResumeSessionBuilder::with_mcp_server` for resumed sessions. With
148//! `unstable_session_fork`, `v2::ForkSessionBuilder::with_mcp_server` provides the
149//! same attachment for forked sessions. These APIs additionally require
150//! `unstable_protocol_v2`. The SDK installs the routes and initially polls the
151//! runners before publishing the setup request, so the agent can use them
152//! during setup or resume replay. Successful attachments remain active for the
153//! connection lifetime; setup failures, including an error response after
154//! cancellation, clean up the pending attachment.
155//!
156//! ```ignore
157//! # use agent_client_protocol::{Client, Agent, ConnectTo};
158//! # use agent_client_protocol::mcp_server::McpServer;
159//! # use agent_client_protocol_rmcp::rmcp_3::McpServerExt;
160//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
161//! # let my_mcp_server = McpServer::<Agent, _>::builder("tools").build();
162//! # Client.v1().connect_with(transport, async |cx| {
163//! cx.build_session_cwd()?
164//!     .with_mcp_server(my_mcp_server)?
165//!     .block_task()
166//!     .run_until(async |session| { Ok(()) })
167//!     .await?;
168//! # Ok(())
169//! # }).await?;
170//! # Ok(())
171//! # }
172//! ```
173//!
174//! See the cookbook for detailed MCP server examples.
175//!
176//! # Non-Blocking Session Start
177//!
178//! If you're inside an `on_receive_*` callback and need to start a session,
179//! use `on_session_start` instead of `block_task().run_until()`:
180//!
181//! ```
182//! # use agent_client_protocol::{Client, Agent, ConnectTo};
183//! # use agent_client_protocol::v1::schema::NewSessionRequest;
184//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
185//! Client.v1()
186//!     .on_receive_request(async |req: NewSessionRequest, responder, cx| {
187//!         cx.build_session_from(req)
188//!             .on_session_start(async |setup| {
189//!                 // Handle `setup.session` and `setup.response`
190//!                 Ok(())
191//!             })?;
192//!         Ok(())
193//!     }, agent_client_protocol::on_receive_request!())
194//! #   .connect_with(transport, async |_| Ok(())).await?;
195//! # Ok(())
196//! # }
197//! ```
198//!
199//! When the session response is routed during its original dispatch, session
200//! routing is installed before later messages are dispatched. The callback is
201//! invoked in a spawned task, so no user callback code has that ordering
202//! guarantee and the callback can wait for session traffic. A response
203//! interceptor that retains and routes the response later cannot retroactively
204//! order setup before messages already processed. See [Ordering](super::ordering)
205//! for details.
206//!
207//! For a draft v2 proxy, use `v2::SessionBuilder::on_proxy_session_start` or
208//! `v2::ResumeSessionBuilder::on_proxy_session_start` instead. The feature-gated
209//! `v2::ForkSessionBuilder` exposes the same helper. Each forwards the complete
210//! operation-specific response and then spawns the callback with an
211//! `v2::SessionSetup`, so the callback keeps both the command-only session
212//! handle and that exact response:
213//!
214//! ```rust,ignore
215//! Proxy.v2()
216//!     .on_receive_request_from(
217//!         Client,
218//!         async |request: v2::schema::NewSessionRequest, responder, cx| {
219//!             cx.build_session_from(request)
220//!                 .on_proxy_session_start(responder, async |setup| {
221//!                     let SessionSetup { session, response, .. } = setup;
222//!                     track_session(session.session_id(), response);
223//!                     Ok(())
224//!                 })
225//!         },
226//!         agent_client_protocol::on_receive_request!(),
227//!     );
228//! ```
229//!
230//! For `session/new` and feature-gated `session/fork`, the builder installs
231//! routing with the newly allocated response session ID before later inbound
232//! traffic is dispatched. For `session/resume`, the builder installs and
233//! acknowledges session routing
234//! before publishing the downstream request, allowing replay updates to be
235//! forwarded before the resume response. The downstream request inherits
236//! upstream cancellation. An unsuccessful downstream response drops pending
237//! routing and MCP attachment; successful setup keeps those routes for the
238//! connection lifetime. The cancellation signal itself remains advisory while
239//! the helper awaits that response. User work runs outside the ordering
240//! barrier. V2 session updates and interactive requests remain independent
241//! traffic handled by typed connection callbacks.
242//!
243//! # Next Steps
244//!
245//! - [Callbacks](super::callbacks) - Handle incoming requests
246//! - [Ordering](super::ordering) - Understand when to use `block_task` vs `on_*`
247//!
248//! [`ActiveSession`]: crate::v1::ActiveSession
249//! [`SessionSetup`]: crate::v1::SessionSetup