Skip to main content

agent_client_protocol/
session.rs

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