Skip to main content

agent_client_protocol/
session.rs

1use std::{future::Future, marker::PhantomData, path::Path};
2
3use futures::channel::{mpsc, oneshot};
4
5use crate::{
6    Agent, Client, ConnectionTo, Dispatch, HandleDispatchFrom, Handled, JsonRpcRequest, Responder,
7    Role,
8    jsonrpc::{
9        DynamicHandlerGuard,
10        run::{NullRun, RunWithConnectionTo},
11    },
12    role::{HasPeer, acp::ProxySessionMessages},
13    schema::v1::{
14        ContentBlock, ContentChunk, LoadSessionRequest, LoadSessionResponse, Meta,
15        NewSessionRequest, NewSessionResponse, PromptRequest, PromptResponse, ResumeSessionRequest,
16        ResumeSessionResponse, SessionConfigOption, SessionId, SessionModeState,
17        SessionNotification, SessionUpdate, StopReason,
18    },
19    util::{MatchDispatch, MatchDispatchFrom, run_until},
20};
21
22#[cfg(feature = "unstable_mcp_over_acp")]
23use crate::{jsonrpc::run::ChainRun, mcp_server::McpServer};
24
25#[cfg(feature = "unstable_protocol_v2")]
26mod v2;
27#[cfg(feature = "unstable_protocol_v2")]
28pub use v2::*;
29
30/// Marker type indicating the session builder will block the current task.
31#[derive(Debug)]
32pub struct Blocking;
33impl SessionBlockState for Blocking {}
34
35/// Marker type indicating the session builder will not block the current task.
36#[derive(Debug)]
37pub struct NonBlocking;
38impl SessionBlockState for NonBlocking {}
39
40/// Trait for marker types that indicate blocking vs non-blocking API.
41/// See [`SessionBuilder::block_task`].
42pub trait SessionBlockState: Send + 'static + Sync + std::fmt::Debug {}
43
44impl<Counterpart: Role> ConnectionTo<Counterpart>
45where
46    Counterpart: HasPeer<Agent>,
47{
48    /// Stable protocol v1 session builder for a new session request.
49    ///
50    /// With `unstable_protocol_v2`, a `Client.v2()` callback receives a
51    /// `V2ConnectionTo` with its own v2 `build_session` helper.
52    pub fn build_session(&self, cwd: impl AsRef<Path>) -> SessionBuilder<Counterpart, NullRun> {
53        SessionBuilder::new(self, NewSessionRequest::new(cwd.as_ref()))
54    }
55
56    /// Stable protocol v1 session builder using the current working directory.
57    ///
58    /// This is a convenience wrapper around [`build_session`](Self::build_session)
59    /// that uses [`std::env::current_dir`] to get the working directory.
60    ///
61    /// Returns an error if the current directory cannot be determined.
62    pub fn build_session_cwd(&self) -> Result<SessionBuilder<Counterpart, NullRun>, crate::Error> {
63        let cwd = std::env::current_dir().map_err(|e| {
64            crate::Error::internal_error().data(format!("cannot get current directory: {e}"))
65        })?;
66        Ok(self.build_session(cwd))
67    }
68
69    /// Stable protocol v1 session builder starting from an existing request.
70    ///
71    /// Use this when you've intercepted a `session.new` request and want to
72    /// modify it (e.g., inject MCP servers) before forwarding.
73    pub fn build_session_from(
74        &self,
75        request: NewSessionRequest,
76    ) -> SessionBuilder<Counterpart, NullRun> {
77        SessionBuilder::new(self, request)
78    }
79
80    /// Stable protocol v1 session builder that loads an existing session.
81    ///
82    /// The returned builder installs session routing before publishing
83    /// `session/load`, so replay notifications sent before the response are
84    /// available through the restored [`ActiveSession`].
85    ///
86    /// Call this only when the initialization response advertises
87    /// `agentCapabilities.loadSession`.
88    pub fn load_session(
89        &self,
90        session_id: impl Into<SessionId>,
91        cwd: impl AsRef<Path>,
92    ) -> RestoreSessionBuilder<Counterpart, LoadSessionRequest> {
93        self.load_session_from(LoadSessionRequest::new(session_id, cwd.as_ref()))
94    }
95
96    /// Stable protocol v1 session builder from an existing `session/load`
97    /// request.
98    ///
99    /// Use this to send a typed request assembled or intercepted elsewhere
100    /// without rebuilding it.
101    pub fn load_session_from(
102        &self,
103        request: LoadSessionRequest,
104    ) -> RestoreSessionBuilder<Counterpart, LoadSessionRequest> {
105        RestoreSessionBuilder::new(self, request)
106    }
107
108    /// Stable protocol v1 session builder that resumes an existing session.
109    ///
110    /// This is the `session/resume` counterpart of
111    /// [`load_session`](Self::load_session), but continues without replaying
112    /// conversation history. Call this only when the initialization response
113    /// advertises `agentCapabilities.sessionCapabilities.resume`.
114    pub fn resume_session(
115        &self,
116        session_id: impl Into<SessionId>,
117        cwd: impl AsRef<Path>,
118    ) -> RestoreSessionBuilder<Counterpart, ResumeSessionRequest> {
119        self.resume_session_from(ResumeSessionRequest::new(session_id, cwd.as_ref()))
120    }
121
122    /// Stable protocol v1 session builder from an existing `session/resume`
123    /// request.
124    ///
125    /// Use this to send a typed request assembled or intercepted elsewhere
126    /// without rebuilding it.
127    pub fn resume_session_from(
128        &self,
129        request: ResumeSessionRequest,
130    ) -> RestoreSessionBuilder<Counterpart, ResumeSessionRequest> {
131        RestoreSessionBuilder::new(self, request)
132    }
133
134    /// Given a session response received from the agent,
135    /// attach a handler to process messages related to this session
136    /// and let you access them.
137    ///
138    /// Normally you would not use this method directly but would
139    /// instead use [`Self::build_session`] and then [`SessionBuilder::start_session`].
140    ///
141    /// The vector `dynamic_handler_registrations` contains any dynamic
142    /// handle registrations associated with this session (e.g., from MCP servers).
143    /// You can simply pass `Default::default()` if not applicable.
144    pub(crate) fn attach_session<'runner>(
145        &self,
146        response: NewSessionResponse,
147        mcp_handler_registrations: Vec<DynamicHandlerGuard<Counterpart>>,
148    ) -> Result<ActiveSession<'runner, Counterpart>, crate::Error> {
149        let NewSessionResponse {
150            session_id,
151            modes,
152            config_options,
153            meta,
154            ..
155        } = response;
156
157        let prepared = self.prepare_session_routing(&session_id)?;
158        Ok(prepared.into_active_session(
159            self.clone(),
160            session_id,
161            modes,
162            config_options,
163            meta,
164            mcp_handler_registrations,
165        ))
166    }
167
168    /// Install the update channel and handler for `session_id`.
169    ///
170    /// Restore requests call this before request publication. Dropping the
171    /// returned value deactivates and removes the route.
172    fn prepare_session_routing(
173        &self,
174        session_id: &SessionId,
175    ) -> Result<PreparedSession<Counterpart>, crate::Error> {
176        let (update_tx, update_rx) = mpsc::unbounded();
177        let handler = ActiveSessionHandler::new(session_id.clone(), update_tx.clone());
178        let session_handler_registration = self.add_dynamic_handler(handler)?;
179
180        Ok(PreparedSession {
181            update_rx,
182            update_tx,
183            session_handler_registration,
184        })
185    }
186}
187
188/// Session-routing state installed before a restore request is published.
189struct PreparedSession<Counterpart: Role>
190where
191    Counterpart: HasPeer<Agent>,
192{
193    update_rx: mpsc::UnboundedReceiver<SessionMessage>,
194    update_tx: mpsc::UnboundedSender<SessionMessage>,
195    session_handler_registration: DynamicHandlerGuard<Counterpart>,
196}
197
198impl<Counterpart> PreparedSession<Counterpart>
199where
200    Counterpart: HasPeer<Agent>,
201{
202    fn into_active_session<'runner>(
203        self,
204        connection: ConnectionTo<Counterpart>,
205        session_id: SessionId,
206        modes: Option<SessionModeState>,
207        config_options: Option<Vec<SessionConfigOption>>,
208        meta: Option<Meta>,
209        mcp_handler_registrations: Vec<DynamicHandlerGuard<Counterpart>>,
210    ) -> ActiveSession<'runner, Counterpart> {
211        ActiveSession {
212            session_id,
213            modes,
214            config_options,
215            meta,
216            update_rx: self.update_rx,
217            update_tx: self.update_tx,
218            connection,
219            session_handler_registration: self.session_handler_registration,
220            mcp_handler_registrations,
221            _runner: PhantomData,
222        }
223    }
224}
225
226/// Internal behavior shared by the two stable restore operations.
227trait RestoreRequest: JsonRpcRequest {
228    fn session_id(&self) -> &SessionId;
229    fn response_modes(response: &Self::Response) -> Option<SessionModeState>;
230    fn response_config_options(response: &Self::Response) -> Option<Vec<SessionConfigOption>>;
231    fn response_meta(response: &Self::Response) -> Option<Meta>;
232}
233
234impl RestoreRequest for LoadSessionRequest {
235    fn session_id(&self) -> &SessionId {
236        &self.session_id
237    }
238
239    fn response_modes(response: &Self::Response) -> Option<SessionModeState> {
240        response.modes.clone()
241    }
242
243    fn response_config_options(response: &Self::Response) -> Option<Vec<SessionConfigOption>> {
244        response.config_options.clone()
245    }
246
247    fn response_meta(response: &Self::Response) -> Option<Meta> {
248        response.meta.clone()
249    }
250}
251
252impl RestoreRequest for ResumeSessionRequest {
253    fn session_id(&self) -> &SessionId {
254        &self.session_id
255    }
256
257    fn response_modes(response: &Self::Response) -> Option<SessionModeState> {
258        response.modes.clone()
259    }
260
261    fn response_config_options(response: &Self::Response) -> Option<Vec<SessionConfigOption>> {
262        response.config_options.clone()
263    }
264
265    fn response_meta(response: &Self::Response) -> Option<Meta> {
266        response.meta.clone()
267    }
268}
269
270/// Stable protocol v1 builder for `session/load` or `session/resume`.
271///
272/// Use [`ConnectionTo::load_session`] or [`ConnectionTo::resume_session`] to
273/// construct this builder. Use the matching `_from` method to send an existing
274/// typed request without rebuilding it.
275///
276/// The `BlockState` parameter mirrors [`SessionBuilder`]:
277/// - [`NonBlocking`] exposes `on_session_start` on each concrete operation.
278/// - [`Blocking`], selected with [`Self::block_task`], exposes
279///   `start_session`.
280///
281/// Session routing is acknowledged before the request can reach the peer.
282/// Dropping a pending blocking start removes that routing and applies the
283/// standard [`SentRequest`](crate::SentRequest) drop-time cancellation
284/// behavior. Error responses remove the route before later entries in the same
285/// transport frame are dispatched.
286#[must_use = "use `start_session` or `on_session_start` to restore the session"]
287#[derive(Debug)]
288pub struct RestoreSessionBuilder<Counterpart, Request, BlockState = NonBlocking>
289where
290    Counterpart: HasPeer<Agent>,
291    BlockState: SessionBlockState,
292{
293    connection: ConnectionTo<Counterpart>,
294    request: Request,
295    block_state: PhantomData<BlockState>,
296}
297
298impl<Counterpart, Request> RestoreSessionBuilder<Counterpart, Request, NonBlocking>
299where
300    Counterpart: HasPeer<Agent>,
301{
302    fn new(connection: &ConnectionTo<Counterpart>, request: Request) -> Self {
303        Self {
304            connection: connection.clone(),
305            request,
306            block_state: PhantomData,
307        }
308    }
309
310    /// Mark this restore builder as able to block the current task.
311    ///
312    /// Do not use the resulting blocking methods inside a message handler.
313    pub fn block_task(self) -> RestoreSessionBuilder<Counterpart, Request, Blocking> {
314        RestoreSessionBuilder {
315            connection: self.connection,
316            request: self.request,
317            block_state: PhantomData,
318        }
319    }
320}
321
322fn restored_session<Counterpart, Request>(
323    connection: ConnectionTo<Counterpart>,
324    session_id: SessionId,
325    prepared: PreparedSession<Counterpart>,
326    response: Request::Response,
327) -> RestoredSession<'static, Counterpart, Request::Response>
328where
329    Counterpart: HasPeer<Agent>,
330    Request: RestoreRequest,
331{
332    let session = prepared.into_active_session(
333        connection,
334        session_id,
335        Request::response_modes(&response),
336        Request::response_config_options(&response),
337        Request::response_meta(&response),
338        Vec::new(),
339    );
340
341    RestoredSession { session, response }
342}
343
344fn on_restore_session_start<Counterpart, Request, F, Fut>(
345    builder: RestoreSessionBuilder<Counterpart, Request>,
346    op: F,
347) -> Result<(), crate::Error>
348where
349    Counterpart: HasPeer<Agent>,
350    Request: RestoreRequest,
351    F: FnOnce(RestoredSession<'static, Counterpart, Request::Response>) -> Fut + Send + 'static,
352    Fut: Future<Output = Result<(), crate::Error>> + Send,
353{
354    ensure_v1_session_protocol(&builder.connection)?;
355
356    let RestoreSessionBuilder {
357        connection,
358        request,
359        block_state: _,
360    } = builder;
361    let session_id = request.session_id().clone();
362    let prepared = connection.prepare_session_routing(&session_id)?;
363    let routing_ready = connection.dynamic_handler_barrier();
364
365    connection
366        .send_ordered_request_to_after(Agent, request, routing_ready)
367        .on_receiving_result({
368            let connection = connection.clone();
369            async move |result| {
370                let response = result?;
371                let restored = restored_session::<_, Request>(
372                    connection.clone(),
373                    session_id,
374                    prepared,
375                    response,
376                );
377                connection.spawn(async move { op(restored).await })
378            }
379        })
380}
381
382async fn start_restored_session<Counterpart, Request>(
383    builder: RestoreSessionBuilder<Counterpart, Request, Blocking>,
384) -> Result<RestoredSession<'static, Counterpart, Request::Response>, crate::Error>
385where
386    Counterpart: HasPeer<Agent>,
387    Request: RestoreRequest,
388{
389    ensure_v1_session_protocol(&builder.connection)?;
390
391    let RestoreSessionBuilder {
392        connection,
393        request,
394        block_state: _,
395    } = builder;
396    let session_id = request.session_id().clone();
397    let prepared = connection.prepare_session_routing(&session_id)?;
398    let routing_ready = connection.dynamic_handler_barrier();
399    let session_connection = connection.clone();
400
401    connection
402        .send_ordered_request_to_after(Agent, request, routing_ready)
403        .block_task_with_ordered_result(move |result| {
404            let response = result?;
405            Ok(restored_session::<_, Request>(
406                session_connection,
407                session_id,
408                prepared,
409                response,
410            ))
411        })
412        .await
413}
414
415impl<Counterpart> RestoreSessionBuilder<Counterpart, LoadSessionRequest>
416where
417    Counterpart: HasPeer<Agent>,
418{
419    /// Restore with `session/load` in the background and run `op` once its
420    /// exact response and active session are available.
421    ///
422    /// This returns immediately and is safe to call from a message handler.
423    /// Replay notifications can arrive before the response and are retained by
424    /// the returned session.
425    pub fn on_session_start<F, Fut>(self, op: F) -> Result<(), crate::Error>
426    where
427        F: FnOnce(RestoredSession<'static, Counterpart, LoadSessionResponse>) -> Fut
428            + Send
429            + 'static,
430        Fut: Future<Output = Result<(), crate::Error>> + Send,
431    {
432        on_restore_session_start(self, op)
433    }
434}
435
436impl<Counterpart> RestoreSessionBuilder<Counterpart, ResumeSessionRequest>
437where
438    Counterpart: HasPeer<Agent>,
439{
440    /// Restore with `session/resume` in the background and run `op` once its
441    /// exact response and active session are available.
442    ///
443    /// This returns immediately and is safe to call from a message handler.
444    /// The returned session receives subsequent session traffic.
445    pub fn on_session_start<F, Fut>(self, op: F) -> Result<(), crate::Error>
446    where
447        F: FnOnce(RestoredSession<'static, Counterpart, ResumeSessionResponse>) -> Fut
448            + Send
449            + 'static,
450        Fut: Future<Output = Result<(), crate::Error>> + Send,
451    {
452        on_restore_session_start(self, op)
453    }
454}
455
456impl<Counterpart> RestoreSessionBuilder<Counterpart, LoadSessionRequest, Blocking>
457where
458    Counterpart: HasPeer<Agent>,
459{
460    /// Publish `session/load`, wait on the current task, and return an
461    /// [`ActiveSession`] together with the exact [`LoadSessionResponse`].
462    ///
463    /// Requires [`block_task`](RestoreSessionBuilder::block_task). Dropping
464    /// this future while it is pending cancels the request and removes the
465    /// provisional session route.
466    pub async fn start_session(
467        self,
468    ) -> Result<RestoredSession<'static, Counterpart, LoadSessionResponse>, crate::Error> {
469        start_restored_session(self).await
470    }
471}
472
473impl<Counterpart> RestoreSessionBuilder<Counterpart, ResumeSessionRequest, Blocking>
474where
475    Counterpart: HasPeer<Agent>,
476{
477    /// Publish `session/resume`, wait on the current task, and return an
478    /// [`ActiveSession`] together with the exact [`ResumeSessionResponse`].
479    ///
480    /// Requires [`block_task`](RestoreSessionBuilder::block_task). Dropping
481    /// this future while it is pending cancels the request and removes the
482    /// provisional session route.
483    pub async fn start_session(
484        self,
485    ) -> Result<RestoredSession<'static, Counterpart, ResumeSessionResponse>, crate::Error> {
486        start_restored_session(self).await
487    }
488}
489
490/// A restored stable-v1 session and the exact operation response that opened
491/// it.
492///
493/// The session ID comes from the load or resume request because stable-v1
494/// restore responses do not repeat it. Keeping the response separate preserves
495/// every operation-specific field without reconstructing it from session
496/// state.
497pub struct RestoredSession<'runner, Link, Response>
498where
499    Link: HasPeer<Agent>,
500{
501    session: ActiveSession<'runner, Link>,
502    response: Response,
503}
504
505impl<'runner, Link, Response> RestoredSession<'runner, Link, Response>
506where
507    Link: HasPeer<Agent>,
508{
509    /// Access the active session.
510    pub fn session(&self) -> &ActiveSession<'runner, Link> {
511        &self.session
512    }
513
514    /// Mutably access the active session, for example to consume replay.
515    pub fn session_mut(&mut self) -> &mut ActiveSession<'runner, Link> {
516        &mut self.session
517    }
518
519    /// Access the complete load or resume response.
520    pub fn response(&self) -> &Response {
521        &self.response
522    }
523
524    /// Split the restored value into its active session and exact response.
525    pub fn into_parts(self) -> (ActiveSession<'runner, Link>, Response) {
526        (self.session, self.response)
527    }
528
529    /// Consume this value and return only the active session.
530    pub fn into_session(self) -> ActiveSession<'runner, Link> {
531        self.session
532    }
533}
534
535impl<Link, Response> std::fmt::Debug for RestoredSession<'_, Link, Response>
536where
537    Link: HasPeer<Agent>,
538    Response: std::fmt::Debug,
539{
540    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
541        formatter
542            .debug_struct("RestoredSession")
543            .field("session_id", self.session.session_id())
544            .field("response", &self.response)
545            .finish()
546    }
547}
548
549/// Stable protocol v1 session builder for a new session request.
550/// Allows you to add MCP servers or set other details for this session.
551///
552/// The `BlockState` type parameter tracks whether blocking methods are available:
553/// - `NonBlocking` (default): Only [`on_session_start`](Self::on_session_start) is available
554/// - `Blocking` (after calling [`block_task`](Self::block_task)):
555///   [`run_until`](Self::run_until) and [`start_session`](Self::start_session) become available
556#[must_use = "use `start_session`, `run_until`, or `on_session_start` to start the session"]
557#[derive(Debug)]
558pub struct SessionBuilder<
559    Counterpart,
560    Run: RunWithConnectionTo<Counterpart> = NullRun,
561    BlockState: SessionBlockState = NonBlocking,
562> where
563    Counterpart: HasPeer<Agent>,
564{
565    connection: ConnectionTo<Counterpart>,
566    request: NewSessionRequest,
567    dynamic_handler_registrations: Vec<DynamicHandlerGuard<Counterpart>>,
568    run: Run,
569    block_state: PhantomData<BlockState>,
570}
571
572impl<Counterpart> SessionBuilder<Counterpart, NullRun, NonBlocking>
573where
574    Counterpart: HasPeer<Agent>,
575{
576    fn new(connection: &ConnectionTo<Counterpart>, request: NewSessionRequest) -> Self {
577        SessionBuilder {
578            connection: connection.clone(),
579            request,
580            dynamic_handler_registrations: Vec::default(),
581            run: NullRun,
582            block_state: PhantomData,
583        }
584    }
585}
586
587impl<Counterpart, R, BlockState> SessionBuilder<Counterpart, R, BlockState>
588where
589    Counterpart: HasPeer<Agent>,
590    R: RunWithConnectionTo<Counterpart>,
591    BlockState: SessionBlockState,
592{
593    /// Attach an MCP server to this new session.
594    #[cfg(feature = "unstable_mcp_over_acp")]
595    pub fn with_mcp_server<McpRun>(
596        mut self,
597        mcp_server: McpServer<Counterpart, McpRun>,
598    ) -> Result<SessionBuilder<Counterpart, ChainRun<R, McpRun>, BlockState>, crate::Error>
599    where
600        McpRun: RunWithConnectionTo<Counterpart>,
601    {
602        let (handler, mcp_run) = mcp_server.into_handler_and_runner();
603        self.dynamic_handler_registrations
604            .push(handler.into_dynamic_handler(&mut self.request, &self.connection)?);
605        Ok(SessionBuilder {
606            connection: self.connection,
607            request: self.request,
608            dynamic_handler_registrations: self.dynamic_handler_registrations,
609            run: ChainRun::new(self.run, mcp_run),
610            block_state: self.block_state,
611        })
612    }
613
614    /// Spawn a task that runs the provided closure once the session starts.
615    ///
616    /// Unlike [`start_session`](Self::start_session), this method returns immediately
617    /// without blocking the current task. The session handshake and closure execution
618    /// happen in a spawned background task.
619    ///
620    /// The closure receives an `ActiveSession<'static, _>` and runs in a
621    /// spawned task. If it returns an error, the error propagates to the
622    /// connection's task handling.
623    ///
624    /// # Example
625    ///
626    /// ```ignore
627    /// # use agent_client_protocol::{Client, Agent, ConnectTo};
628    /// # use agent_client_protocol::mcp_server::McpServer;
629    /// # use agent_client_protocol_rmcp::McpServerExt;
630    /// # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
631    /// # Client.builder().connect_with(transport, async |cx| {
632    /// # let mcp = McpServer::<Agent, _>::builder("tools").build();
633    /// cx.build_session_cwd()?
634    ///     .with_mcp_server(mcp)?
635    ///     .on_session_start(async |mut session| {
636    ///         // Do something with the session
637    ///         session.send_prompt("Hello")?;
638    ///         let response = session.read_to_string().await?;
639    ///         Ok(())
640    ///     })?;
641    /// // Returns immediately, session runs in background
642    /// # Ok(())
643    /// # }).await?;
644    /// # Ok(())
645    /// # }
646    /// ```
647    ///
648    /// # Ordering
649    ///
650    /// Session runners are scheduled and routing setup is installed before the
651    /// dispatch loop processes the next message when the session response is
652    /// routed during its original dispatch. No user callback code runs under
653    /// that ordering guarantee: the callback is invoked in a spawned task, so
654    /// it may wait for later session traffic without deadlocking the connection.
655    /// A response interceptor that retains the response and routes it later
656    /// cannot retroactively order session setup before messages the dispatch
657    /// loop has already processed.
658    pub fn on_session_start<F, Fut>(self, op: F) -> Result<(), crate::Error>
659    where
660        R: 'static,
661        F: FnOnce(ActiveSession<'static, Counterpart>) -> Fut + Send + 'static,
662        Fut: Future<Output = Result<(), crate::Error>> + Send,
663    {
664        ensure_v1_session_protocol(&self.connection)?;
665
666        let Self {
667            connection,
668            request,
669            dynamic_handler_registrations,
670            run,
671            block_state: _,
672        } = self;
673
674        connection
675            .send_ordered_request_to(Agent, request)
676            .on_receiving_result({
677                let connection = connection.clone();
678                async move |result| {
679                    let response = result?;
680
681                    connection.spawn(run.run_with_connection_to(connection.clone()))?;
682
683                    let active_session =
684                        connection.attach_session(response, dynamic_handler_registrations)?;
685
686                    connection.spawn(async move { op(active_session).await })
687                }
688            })
689    }
690
691    /// Spawn a proxy session and run a closure with the session ID.
692    ///
693    /// A **proxy session** starts the session with the agent and then automatically
694    /// proxies all session updates (prompts, tool calls, etc.) from the agent back
695    /// to the client. You don't need to handle any messages yourself - the proxy
696    /// takes care of forwarding everything. This is useful when you want to inject
697    /// and/or filter prompts coming from the client but otherwise not be involved
698    /// in the session.
699    ///
700    /// Unlike [`start_session_proxy`](Self::start_session_proxy), this method returns
701    /// immediately without blocking the current task. The session handshake, client
702    /// response, and proxy setup all happen in a spawned background task.
703    ///
704    /// The closure receives the `SessionId` once the session is established. Use it for logging
705    /// or eventual tracking; it runs concurrently with later connection traffic. Register
706    /// ID-independent state that later handlers must observe before calling this helper. For
707    /// ID-keyed bookkeeping, install a gate or placeholder first, make later handlers await it,
708    /// and populate it from the closure.
709    ///
710    /// # Example
711    ///
712    /// ```ignore
713    /// # use agent_client_protocol::{Proxy, Client, Conductor, ConnectTo};
714    /// # use agent_client_protocol::schema::v1::NewSessionRequest;
715    /// # use agent_client_protocol::mcp_server::McpServer;
716    /// # use agent_client_protocol_rmcp::McpServerExt;
717    /// # async fn example(transport: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
718    /// Proxy.builder()
719    ///     .on_receive_request_from(Client, async |request: NewSessionRequest, responder, cx| {
720    ///         let mcp = McpServer::<Conductor, _>::builder("tools").build();
721    ///         cx.build_session_from(request)
722    ///             .with_mcp_server(mcp)?
723    ///             .on_proxy_session_start(responder, async |session_id| {
724    ///                 // Session started
725    ///                 Ok(())
726    ///             })
727    ///     }, agent_client_protocol::on_receive_request!())
728    ///     .connect_to(transport)
729    ///     .await?;
730    /// # Ok(())
731    /// # }
732    /// ```
733    ///
734    /// # Ordering
735    ///
736    /// The client response is queued, proxy routing is installed, and session runners are
737    /// scheduled before the dispatch loop processes the next message when the session response
738    /// is routed during its original dispatch. This is a local ordering guarantee, not a
739    /// guarantee that the response reaches the client before later wire traffic. No user callback
740    /// code runs under the barrier: the callback is invoked in a spawned task, so it may wait for
741    /// later connection traffic. A response interceptor that retains the response and routes it
742    /// later cannot retroactively order this setup before messages the loop already processed.
743    pub fn on_proxy_session_start<F, Fut>(
744        self,
745        responder: Responder<NewSessionResponse>,
746        op: F,
747    ) -> Result<(), crate::Error>
748    where
749        F: FnOnce(SessionId) -> Fut + Send + 'static,
750        Fut: Future<Output = Result<(), crate::Error>> + Send,
751        Counterpart: HasPeer<Client>,
752        R: 'static,
753    {
754        ensure_v1_session_protocol(&self.connection)?;
755
756        let Self {
757            connection,
758            request,
759            dynamic_handler_registrations,
760            run,
761            block_state: _,
762        } = self;
763
764        // Send the "new session" request to the agent.
765        let sent = connection.send_ordered_request_to(Agent, request);
766        let sent = sent.forward_cancellation_from(responder.cancellation());
767
768        sent.on_receiving_ok_result(responder, {
769            let connection = connection.clone();
770            async move |response, responder| {
771                // Extract the session-id from the response and forward
772                // the response back to the client
773                let session_id = response.session_id.clone();
774                responder.respond(response)?;
775
776                // Install a dynamic handler to proxy messages from this session
777                connection
778                    .add_dynamic_handler(ProxySessionMessages::new(session_id.clone()))?
779                    .detach();
780
781                // Spawn off the run and dynamic handlers to run indefinitely
782                connection.spawn(run.run_with_connection_to(connection.clone()))?;
783                dynamic_handler_registrations
784                    .into_iter()
785                    .for_each(DynamicHandlerGuard::detach);
786
787                connection.spawn(async move { op(session_id).await })
788            }
789        })
790    }
791}
792
793impl<Counterpart, R> SessionBuilder<Counterpart, R, NonBlocking>
794where
795    Counterpart: HasPeer<Agent>,
796    R: RunWithConnectionTo<Counterpart>,
797{
798    /// Mark this session builder as being able to block the current task.
799    ///
800    /// After calling this, you can use [`run_until`](Self::run_until) or
801    /// [`start_session`](Self::start_session) which block the current task.
802    ///
803    /// This should not be used from inside a message handler like
804    /// [`Builder::on_receive_request`](`crate::Builder::on_receive_request`) or [`HandleDispatchFrom`]
805    /// implementations.
806    pub fn block_task(self) -> SessionBuilder<Counterpart, R, Blocking> {
807        SessionBuilder {
808            connection: self.connection,
809            request: self.request,
810            dynamic_handler_registrations: self.dynamic_handler_registrations,
811            run: self.run,
812            block_state: PhantomData,
813        }
814    }
815}
816
817impl<Counterpart, R> SessionBuilder<Counterpart, R, Blocking>
818where
819    Counterpart: HasPeer<Agent>,
820    R: RunWithConnectionTo<Counterpart>,
821{
822    /// Run this session synchronously. The current task will be blocked
823    /// and `op` will be executed with the active session information.
824    /// This is useful when you have MCP servers that are borrowed from your local
825    /// stack frame.
826    ///
827    /// The `ActiveSession` passed to `op` has a non-`'static` lifetime, which
828    /// prevents calling [`ActiveSession::proxy_remaining_messages`] (since the
829    /// session's background runners would terminate when `op` returns).
830    ///
831    /// Requires calling [`block_task`](Self::block_task) first.
832    pub async fn run_until<T>(
833        self,
834        op: impl for<'runner> AsyncFnOnce(
835            ActiveSession<'runner, Counterpart>,
836        ) -> Result<T, crate::Error>,
837    ) -> Result<T, crate::Error> {
838        ensure_v1_session_protocol(&self.connection)?;
839
840        let Self {
841            connection,
842            request,
843            dynamic_handler_registrations,
844            run,
845            block_state: _,
846        } = self;
847
848        let response = connection
849            .send_request_to(Agent, request)
850            .block_task()
851            .await?;
852
853        let active_session = connection.attach_session(response, dynamic_handler_registrations)?;
854
855        run_until(
856            run.run_with_connection_to(connection.clone()),
857            op(active_session),
858        )
859        .await
860    }
861
862    /// Send the request to create the session and return a handle.
863    /// This is an alternative to [`Self::run_until`] that avoids rightward
864    /// drift but at the cost of requiring MCP servers that are `Send` and
865    /// don't access data from the surrounding scope.
866    ///
867    /// Returns an `ActiveSession<'static, _>` because the session's runners are spawned into
868    /// background tasks that live for the connection lifetime.
869    ///
870    /// Requires calling [`block_task`](Self::block_task) first.
871    pub async fn start_session(self) -> Result<ActiveSession<'static, Counterpart>, crate::Error>
872    where
873        R: 'static,
874    {
875        ensure_v1_session_protocol(&self.connection)?;
876
877        let Self {
878            connection,
879            request,
880            dynamic_handler_registrations,
881            run,
882            block_state: _,
883        } = self;
884
885        let (active_session_tx, active_session_rx) = oneshot::channel();
886
887        connection.clone().spawn(async move {
888            let response = connection
889                .send_request_to(Agent, request)
890                .block_task()
891                .await?;
892
893            connection.spawn(run.run_with_connection_to(connection.clone()))?;
894
895            let active_session =
896                connection.attach_session(response, dynamic_handler_registrations)?;
897
898            active_session_tx
899                .send(active_session)
900                .map_err(|_| crate::Error::internal_error())?;
901
902            Ok(())
903        })?;
904
905        active_session_rx
906            .await
907            .map_err(|_| crate::Error::internal_error())
908    }
909
910    /// Start a proxy session that forwards all messages between client and agent.
911    ///
912    /// A **proxy session** starts the session with the agent and then automatically
913    /// proxies all session updates (prompts, tool calls, etc.) from the agent back
914    /// to the client. You don't need to handle any messages yourself - the proxy
915    /// takes care of forwarding everything. This is useful when you want to inject
916    /// and/or filter prompts coming from the client but otherwise not be involved
917    /// in the session.
918    ///
919    /// This is a convenience method that combines [`start_session`](Self::start_session),
920    /// responding to the client, and [`ActiveSession::proxy_remaining_messages`].
921    ///
922    /// For more control (e.g., to send some messages before proxying), use
923    /// [`start_session`](Self::start_session) instead and call
924    /// [`proxy_remaining_messages`](ActiveSession::proxy_remaining_messages) manually.
925    ///
926    /// Requires calling [`block_task`](Self::block_task) first.
927    pub async fn start_session_proxy(
928        self,
929        responder: Responder<NewSessionResponse>,
930    ) -> Result<SessionId, crate::Error>
931    where
932        Counterpart: HasPeer<Client>,
933        R: 'static,
934    {
935        let active_session = self.start_session().await?;
936        let session_id = active_session.session_id().clone();
937        responder.respond(active_session.response())?;
938        active_session.proxy_remaining_messages()?;
939        Ok(session_id)
940    }
941}
942
943/// Stable protocol v1 active session that lets you send prompts and receive updates.
944///
945/// The `'runner` lifetime represents the span during which session support runners
946/// (such as MCP servers) are active. When created via [`SessionBuilder::start_session`],
947/// this is `'static` because the runners are spawned into background tasks.
948/// When created via [`SessionBuilder::run_until`], this is tied to the
949/// closure scope, preventing [`Self::proxy_remaining_messages`] from being called
950/// (since the runners would stop when the closure returns).
951#[derive(Debug)]
952pub struct ActiveSession<'runner, Link>
953where
954    Link: HasPeer<Agent>,
955{
956    session_id: SessionId,
957    update_rx: mpsc::UnboundedReceiver<SessionMessage>,
958    update_tx: mpsc::UnboundedSender<SessionMessage>,
959    modes: Option<SessionModeState>,
960    config_options: Option<Vec<SessionConfigOption>>,
961    meta: Option<serde_json::Map<String, serde_json::Value>>,
962    connection: ConnectionTo<Link>,
963
964    /// Registration for the handler that routes session messages to `update_rx`.
965    /// This is separate from MCP handlers so it can be dropped independently
966    /// when switching to proxy mode.
967    session_handler_registration: DynamicHandlerGuard<Link>,
968
969    /// Registrations for MCP server handlers.
970    /// These will be dropped once the active-session struct is dropped
971    /// which will cause them to be deregistered.
972    mcp_handler_registrations: Vec<DynamicHandlerGuard<Link>>,
973
974    /// Phantom lifetime representing the session-runner lifetime.
975    _runner: PhantomData<&'runner ()>,
976}
977
978/// Incoming stable protocol v1 message from the agent.
979#[non_exhaustive]
980#[derive(Debug)]
981#[allow(
982    clippy::large_enum_variant,
983    reason = "Dispatch messages vastly outnumber StopReason; boxing would add a heap allocation"
984)]
985pub enum SessionMessage {
986    /// Periodic updates with new content, tool requests, etc.
987    /// Use [`MatchDispatch`] to match on the message type.
988    SessionMessage(Dispatch),
989
990    /// When a prompt completes, the stop reason.
991    StopReason(StopReason),
992}
993
994impl<Link> ActiveSession<'_, Link>
995where
996    Link: HasPeer<Agent>,
997{
998    /// Access the session ID.
999    pub fn session_id(&self) -> &SessionId {
1000        &self.session_id
1001    }
1002
1003    /// Access modes available in this session.
1004    pub fn modes(&self) -> Option<&SessionModeState> {
1005        self.modes.as_ref()
1006    }
1007
1008    /// Access the initial session configuration options returned by the agent.
1009    pub fn config_options(&self) -> Option<&[SessionConfigOption]> {
1010        self.config_options.as_deref()
1011    }
1012
1013    /// Access meta data from session response.
1014    pub fn meta(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
1015        self.meta.as_ref()
1016    }
1017
1018    /// Build a `NewSessionResponse` from the session information.
1019    ///
1020    /// Useful when you need to forward the session response to a client
1021    /// after doing some processing.
1022    pub fn response(&self) -> NewSessionResponse {
1023        NewSessionResponse::new(self.session_id.clone())
1024            .modes(self.modes.clone())
1025            .config_options(self.config_options.clone())
1026            .meta(self.meta.clone())
1027    }
1028
1029    /// Access the underlying connection context used to communicate with the agent.
1030    pub fn connection(&self) -> &ConnectionTo<Link> {
1031        &self.connection
1032    }
1033
1034    /// Send a prompt to the agent. You can then read messages sent in response.
1035    pub fn send_prompt(&mut self, prompt: impl ToString) -> Result<(), crate::Error> {
1036        let update_tx = self.update_tx.clone();
1037        self.connection
1038            .send_ordered_request_to(
1039                Agent,
1040                PromptRequest::new(self.session_id.clone(), vec![prompt.to_string().into()]),
1041            )
1042            .on_receiving_result(async move |result| {
1043                let PromptResponse { stop_reason, .. } = result?;
1044
1045                update_tx
1046                    .unbounded_send(SessionMessage::StopReason(stop_reason))
1047                    .map_err(crate::util::internal_error)?;
1048
1049                Ok(())
1050            })
1051    }
1052
1053    /// Read an update from the agent in response to the prompt.
1054    pub async fn read_update(&mut self) -> Result<SessionMessage, crate::Error> {
1055        use futures::StreamExt;
1056        let message =
1057            self.update_rx.next().await.ok_or_else(|| {
1058                crate::util::internal_error("session channel closed unexpectedly")
1059            })?;
1060
1061        Ok(message)
1062    }
1063
1064    /// Read all updates until the end of the turn and create a string.
1065    /// Ignores non-text updates.
1066    pub async fn read_to_string(&mut self) -> Result<String, crate::Error> {
1067        let mut output = String::new();
1068        loop {
1069            let update = self.read_update().await?;
1070            tracing::trace!(?update, "read_to_string update");
1071            match update {
1072                SessionMessage::SessionMessage(dispatch) => MatchDispatch::new(dispatch)
1073                    .if_notification(async |notif: SessionNotification| match notif.update {
1074                        SessionUpdate::AgentMessageChunk(ContentChunk {
1075                            content: ContentBlock::Text(text),
1076                            ..
1077                        }) => {
1078                            output.push_str(&text.text);
1079                            Ok(())
1080                        }
1081                        _ => Ok(()),
1082                    })
1083                    .await
1084                    .otherwise_ignore()?,
1085                SessionMessage::StopReason(_stop_reason) => break,
1086            }
1087        }
1088        Ok(output)
1089    }
1090}
1091
1092impl<Link> ActiveSession<'static, Link>
1093where
1094    Link: HasPeer<Agent>,
1095{
1096    /// Proxy all remaining messages for this session between client and agent.
1097    ///
1098    /// Use this when you want to inject MCP servers into a session but don't need
1099    /// to actively interact with it after setup. The session messages will be proxied
1100    /// between client and agent automatically.
1101    ///
1102    /// This consumes the `ActiveSession` since you're giving up active control.
1103    ///
1104    /// This method is only available on `ActiveSession<'static, _>` (from
1105    /// [`SessionBuilder::start_session`]) because it requires the session's runners to outlive
1106    /// the method call.
1107    ///
1108    /// # Message Ordering Guarantees
1109    ///
1110    /// This method ensures proper handoff from active session mode to proxy mode
1111    /// without losing or reordering messages:
1112    ///
1113    /// 1. **Stop the session handler** - Drop the registration that routes messages
1114    ///    to `update_rx`. After this, no new messages will be queued.
1115    /// 2. **Close the channel** - Drop `update_tx` so we can detect when the channel
1116    ///    is fully drained.
1117    /// 3. **Drain queued messages** - Forward any messages that were already queued
1118    ///    in `update_rx` to the client, preserving order.
1119    /// 4. **Install proxy handler** - Now that all queued messages are forwarded,
1120    ///    install the proxy handler to handle future messages.
1121    ///
1122    /// This sequence prevents the race condition where messages could be delivered
1123    /// out of order or lost during the transition.
1124    pub fn proxy_remaining_messages(self) -> Result<(), crate::Error>
1125    where
1126        Link: HasPeer<Client>,
1127    {
1128        // Destructure self to get ownership of all fields
1129        let ActiveSession {
1130            session_id,
1131            mut update_rx,
1132            update_tx,
1133            connection,
1134            session_handler_registration,
1135            mcp_handler_registrations,
1136            // These fields are not needed for proxying
1137            modes: _,
1138            config_options: _,
1139            meta: _,
1140            _runner,
1141        } = self;
1142
1143        // Step 1: Drop the session handler registration.
1144        // This unregisters the handler that was routing messages to update_rx.
1145        // After this point, no new messages will be added to the channel.
1146        drop(session_handler_registration);
1147
1148        // Step 2: Drop the sender side of the channel.
1149        // This allows us to detect when the channel is fully drained
1150        // (recv will return None when empty and sender is dropped).
1151        drop(update_tx);
1152
1153        // Step 3: Drain any messages that were already queued and forward to client.
1154        // These messages arrived before we dropped the handler but haven't been
1155        // consumed yet. We must forward them to maintain message ordering.
1156        while let Ok(message) = update_rx.try_recv() {
1157            match message {
1158                SessionMessage::SessionMessage(dispatch) => {
1159                    // Forward the message to the client
1160                    connection.send_proxied_message_to(Client, dispatch)?;
1161                }
1162                SessionMessage::StopReason(_) => {
1163                    // StopReason is internal bookkeeping, not forwarded
1164                }
1165            }
1166        }
1167
1168        // Step 4: Install the proxy handler for future messages.
1169        // Now that all queued messages have been forwarded, the proxy handler
1170        // can take over. Any new messages will go directly through the proxy.
1171        connection
1172            .add_dynamic_handler(ProxySessionMessages::new(session_id))?
1173            .detach();
1174
1175        // Keep MCP server handlers alive for the lifetime of the proxy
1176        for registration in mcp_handler_registrations {
1177            registration.detach();
1178        }
1179
1180        Ok(())
1181    }
1182}
1183
1184struct ActiveSessionHandler {
1185    session_id: SessionId,
1186    update_tx: mpsc::UnboundedSender<SessionMessage>,
1187}
1188
1189impl ActiveSessionHandler {
1190    pub fn new(session_id: SessionId, update_tx: mpsc::UnboundedSender<SessionMessage>) -> Self {
1191        Self {
1192            session_id,
1193            update_tx,
1194        }
1195    }
1196}
1197
1198impl<Counterpart: Role> HandleDispatchFrom<Counterpart> for ActiveSessionHandler
1199where
1200    Counterpart: HasPeer<Agent>,
1201{
1202    async fn handle_dispatch_from(
1203        &mut self,
1204        message: Dispatch,
1205        cx: ConnectionTo<Counterpart>,
1206    ) -> Result<Handled<Dispatch>, crate::Error> {
1207        // If this is a message for our session, grab it.
1208        tracing::trace!(
1209            ?message,
1210            handler_session_id = ?self.session_id,
1211            "ActiveSessionHandler::handle_dispatch"
1212        );
1213        MatchDispatchFrom::new(message, &cx)
1214            .if_dispatch_from(Agent, async |message| {
1215                if let Some(session_id) = message.get_session_id()? {
1216                    tracing::trace!(
1217                        message_session_id = ?session_id,
1218                        handler_session_id = ?self.session_id,
1219                        "ActiveSessionHandler::handle_dispatch"
1220                    );
1221                    if session_id == self.session_id {
1222                        self.update_tx
1223                            .unbounded_send(SessionMessage::SessionMessage(message))
1224                            .map_err(crate::util::internal_error)?;
1225                        return Ok(Handled::Yes);
1226                    }
1227                }
1228
1229                // Otherwise, pass it through.
1230                Ok(Handled::No {
1231                    message,
1232                    retry: false,
1233                })
1234            })
1235            .await
1236            .done()
1237    }
1238
1239    fn describe_chain(&self) -> impl std::fmt::Debug {
1240        format!("ActiveSessionHandler({})", self.session_id)
1241    }
1242}
1243
1244#[cfg(not(feature = "unstable_protocol_v2"))]
1245#[allow(
1246    clippy::unnecessary_wraps,
1247    reason = "signature matches the feature-enabled protocol guard"
1248)]
1249fn ensure_v1_session_protocol<Counterpart: Role>(
1250    _connection: &ConnectionTo<Counterpart>,
1251) -> Result<(), crate::Error> {
1252    Ok(())
1253}
1254
1255#[cfg(feature = "unstable_protocol_v2")]
1256fn ensure_v1_session_protocol<Counterpart: Role>(
1257    connection: &ConnectionTo<Counterpart>,
1258) -> Result<(), crate::Error> {
1259    if connection.acp_protocol_version() != Some(crate::schema::ProtocolVersion::V2) {
1260        return Ok(());
1261    }
1262
1263    Err(crate::Error::invalid_request().data(
1264        "stable session builders use ACP protocol v1 types, but this is a protocol v2 connection; \
1265         use the `V2ConnectionTo` supplied to `Client.v2()` callbacks",
1266    ))
1267}