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