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, Responder, Role,
7    jsonrpc::{
8        DynamicHandlerGuard,
9        run::{NullRun, RunWithConnectionTo},
10    },
11    role::{HasPeer, acp::ProxySessionMessages},
12    schema::v1::{
13        ContentBlock, ContentChunk, NewSessionRequest, NewSessionResponse, PromptRequest,
14        PromptResponse, SessionId, SessionModeState, SessionNotification, SessionUpdate,
15        StopReason,
16    },
17    util::{MatchDispatch, MatchDispatchFrom, run_until},
18};
19
20#[cfg(feature = "unstable_mcp_over_acp")]
21use crate::{jsonrpc::run::ChainRun, mcp_server::McpServer};
22
23/// Marker type indicating the session builder will block the current task.
24#[derive(Debug)]
25pub struct Blocking;
26impl SessionBlockState for Blocking {}
27
28/// Marker type indicating the session builder will not block the current task.
29#[derive(Debug)]
30pub struct NonBlocking;
31impl SessionBlockState for NonBlocking {}
32
33/// Trait for marker types that indicate blocking vs non-blocking API.
34/// See [`SessionBuilder::block_task`].
35pub trait SessionBlockState: Send + 'static + Sync + std::fmt::Debug {}
36
37impl<Counterpart: Role> ConnectionTo<Counterpart>
38where
39    Counterpart: HasPeer<Agent>,
40{
41    /// Session builder for a new session request.
42    pub fn build_session(&self, cwd: impl AsRef<Path>) -> SessionBuilder<Counterpart, NullRun> {
43        SessionBuilder::new(self, NewSessionRequest::new(cwd.as_ref()))
44    }
45
46    /// Session builder using the current working directory.
47    ///
48    /// This is a convenience wrapper around [`build_session`](Self::build_session)
49    /// that uses [`std::env::current_dir`] to get the working directory.
50    ///
51    /// Returns an error if the current directory cannot be determined.
52    pub fn build_session_cwd(&self) -> Result<SessionBuilder<Counterpart, NullRun>, crate::Error> {
53        let cwd = std::env::current_dir().map_err(|e| {
54            crate::Error::internal_error().data(format!("cannot get current directory: {e}"))
55        })?;
56        Ok(self.build_session(cwd))
57    }
58
59    /// Session builder starting from an existing request.
60    ///
61    /// Use this when you've intercepted a `session.new` request and want to
62    /// modify it (e.g., inject MCP servers) before forwarding.
63    pub fn build_session_from(
64        &self,
65        request: NewSessionRequest,
66    ) -> SessionBuilder<Counterpart, NullRun> {
67        SessionBuilder::new(self, request)
68    }
69
70    /// Given a session response received from the agent,
71    /// attach a handler to process messages related to this session
72    /// and let you access them.
73    ///
74    /// Normally you would not use this method directly but would
75    /// instead use [`Self::build_session`] and then [`SessionBuilder::start_session`].
76    ///
77    /// The vector `dynamic_handler_registrations` contains any dynamic
78    /// handle registrations associated with this session (e.g., from MCP servers).
79    /// You can simply pass `Default::default()` if not applicable.
80    pub(crate) fn attach_session<'runner>(
81        &self,
82        response: NewSessionResponse,
83        mcp_handler_registrations: Vec<DynamicHandlerGuard<Counterpart>>,
84    ) -> Result<ActiveSession<'runner, Counterpart>, crate::Error> {
85        let NewSessionResponse {
86            session_id,
87            modes,
88            meta,
89            ..
90        } = response;
91
92        let (update_tx, update_rx) = mpsc::unbounded();
93        let handler = ActiveSessionHandler::new(session_id.clone(), update_tx.clone());
94        let session_handler_registration = self.add_dynamic_handler(handler)?;
95
96        Ok(ActiveSession {
97            session_id,
98            modes,
99            meta,
100            update_rx,
101            update_tx,
102            connection: self.clone(),
103            session_handler_registration,
104            mcp_handler_registrations,
105            _runner: PhantomData,
106        })
107    }
108}
109
110/// Session builder for a new session request.
111/// Allows you to add MCP servers or set other details for this session.
112///
113/// The `BlockState` type parameter tracks whether blocking methods are available:
114/// - `NonBlocking` (default): Only [`on_session_start`](Self::on_session_start) is available
115/// - `Blocking` (after calling [`block_task`](Self::block_task)):
116///   [`run_until`](Self::run_until) and [`start_session`](Self::start_session) become available
117#[must_use = "use `start_session`, `run_until`, or `on_session_start` to start the session"]
118#[derive(Debug)]
119pub struct SessionBuilder<
120    Counterpart,
121    Run: RunWithConnectionTo<Counterpart> = NullRun,
122    BlockState: SessionBlockState = NonBlocking,
123> where
124    Counterpart: HasPeer<Agent>,
125{
126    connection: ConnectionTo<Counterpart>,
127    request: NewSessionRequest,
128    dynamic_handler_registrations: Vec<DynamicHandlerGuard<Counterpart>>,
129    run: Run,
130    block_state: PhantomData<BlockState>,
131}
132
133impl<Counterpart> SessionBuilder<Counterpart, NullRun, NonBlocking>
134where
135    Counterpart: HasPeer<Agent>,
136{
137    fn new(connection: &ConnectionTo<Counterpart>, request: NewSessionRequest) -> Self {
138        SessionBuilder {
139            connection: connection.clone(),
140            request,
141            dynamic_handler_registrations: Vec::default(),
142            run: NullRun,
143            block_state: PhantomData,
144        }
145    }
146}
147
148impl<Counterpart, R, BlockState> SessionBuilder<Counterpart, R, BlockState>
149where
150    Counterpart: HasPeer<Agent>,
151    R: RunWithConnectionTo<Counterpart>,
152    BlockState: SessionBlockState,
153{
154    /// Attach an MCP server to this new session.
155    #[cfg(feature = "unstable_mcp_over_acp")]
156    #[cfg_attr(docsrs, doc(cfg(feature = "unstable_mcp_over_acp")))]
157    pub fn with_mcp_server<McpRun>(
158        mut self,
159        mcp_server: McpServer<Counterpart, McpRun>,
160    ) -> Result<SessionBuilder<Counterpart, ChainRun<R, McpRun>, BlockState>, crate::Error>
161    where
162        McpRun: RunWithConnectionTo<Counterpart>,
163    {
164        let (handler, mcp_run) = mcp_server.into_handler_and_runner();
165        self.dynamic_handler_registrations
166            .push(handler.into_dynamic_handler(&mut self.request, &self.connection)?);
167        Ok(SessionBuilder {
168            connection: self.connection,
169            request: self.request,
170            dynamic_handler_registrations: self.dynamic_handler_registrations,
171            run: ChainRun::new(self.run, mcp_run),
172            block_state: self.block_state,
173        })
174    }
175
176    /// Spawn a task that runs the provided closure once the session starts.
177    ///
178    /// Unlike [`start_session`](Self::start_session), this method returns immediately
179    /// without blocking the current task. The session handshake and closure execution
180    /// happen in a spawned background task.
181    ///
182    /// The closure receives an `ActiveSession<'static, _>` and runs in a
183    /// spawned task. If it returns an error, the error propagates to the
184    /// connection's task handling.
185    ///
186    /// # Example
187    ///
188    /// ```ignore
189    /// # use agent_client_protocol::{Client, Agent, ConnectTo};
190    /// # use agent_client_protocol::mcp_server::McpServer;
191    /// # use agent_client_protocol_rmcp::McpServerExt;
192    /// # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
193    /// # Client.builder().connect_with(transport, async |cx| {
194    /// # let mcp = McpServer::<Agent, _>::builder("tools").build();
195    /// cx.build_session_cwd()?
196    ///     .with_mcp_server(mcp)?
197    ///     .on_session_start(async |mut session| {
198    ///         // Do something with the session
199    ///         session.send_prompt("Hello")?;
200    ///         let response = session.read_to_string().await?;
201    ///         Ok(())
202    ///     })?;
203    /// // Returns immediately, session runs in background
204    /// # Ok(())
205    /// # }).await?;
206    /// # Ok(())
207    /// # }
208    /// ```
209    ///
210    /// # Ordering
211    ///
212    /// Session runners are scheduled and routing setup is installed before the
213    /// dispatch loop processes the next message when the session response is
214    /// routed during its original dispatch. No user callback code runs under
215    /// that ordering guarantee: the callback is invoked in a spawned task, so
216    /// it may wait for later session traffic without deadlocking the connection.
217    /// A response interceptor that retains the response and routes it later
218    /// cannot retroactively order session setup before messages the dispatch
219    /// loop has already processed.
220    pub fn on_session_start<F, Fut>(self, op: F) -> Result<(), crate::Error>
221    where
222        R: 'static,
223        F: FnOnce(ActiveSession<'static, Counterpart>) -> Fut + Send + 'static,
224        Fut: Future<Output = Result<(), crate::Error>> + Send,
225    {
226        let Self {
227            connection,
228            request,
229            dynamic_handler_registrations,
230            run,
231            block_state: _,
232        } = self;
233
234        connection
235            .send_request_to(Agent, request)
236            .on_receiving_result({
237                let connection = connection.clone();
238                async move |result| {
239                    let response = result?;
240
241                    connection.spawn(run.run_with_connection_to(connection.clone()))?;
242
243                    let active_session =
244                        connection.attach_session(response, dynamic_handler_registrations)?;
245
246                    connection.spawn(async move { op(active_session).await })
247                }
248            })
249    }
250
251    /// Spawn a proxy session and run a closure with the session ID.
252    ///
253    /// A **proxy session** starts the session with the agent and then automatically
254    /// proxies all session updates (prompts, tool calls, etc.) from the agent back
255    /// to the client. You don't need to handle any messages yourself - the proxy
256    /// takes care of forwarding everything. This is useful when you want to inject
257    /// and/or filter prompts coming from the client but otherwise not be involved
258    /// in the session.
259    ///
260    /// Unlike [`start_session_proxy`](Self::start_session_proxy), this method returns
261    /// immediately without blocking the current task. The session handshake, client
262    /// response, and proxy setup all happen in a spawned background task.
263    ///
264    /// The closure receives the `SessionId` once the session is established. Use it for logging
265    /// or eventual tracking; it runs concurrently with later connection traffic. Register
266    /// ID-independent state that later handlers must observe before calling this helper. For
267    /// ID-keyed bookkeeping, install a gate or placeholder first, make later handlers await it,
268    /// and populate it from the closure.
269    ///
270    /// # Example
271    ///
272    /// ```ignore
273    /// # use agent_client_protocol::{Proxy, Client, Conductor, ConnectTo};
274    /// # use agent_client_protocol::schema::v1::NewSessionRequest;
275    /// # use agent_client_protocol::mcp_server::McpServer;
276    /// # use agent_client_protocol_rmcp::McpServerExt;
277    /// # async fn example(transport: impl ConnectTo<Proxy>) -> Result<(), agent_client_protocol::Error> {
278    /// Proxy.builder()
279    ///     .on_receive_request_from(Client, async |request: NewSessionRequest, responder, cx| {
280    ///         let mcp = McpServer::<Conductor, _>::builder("tools").build();
281    ///         cx.build_session_from(request)
282    ///             .with_mcp_server(mcp)?
283    ///             .on_proxy_session_start(responder, async |session_id| {
284    ///                 // Session started
285    ///                 Ok(())
286    ///             })
287    ///     }, agent_client_protocol::on_receive_request!())
288    ///     .connect_to(transport)
289    ///     .await?;
290    /// # Ok(())
291    /// # }
292    /// ```
293    ///
294    /// # Ordering
295    ///
296    /// The client response is queued, proxy routing is installed, and session runners are
297    /// scheduled before the dispatch loop processes the next message when the session response
298    /// is routed during its original dispatch. This is a local ordering guarantee, not a
299    /// guarantee that the response reaches the client before later wire traffic. No user callback
300    /// code runs under the barrier: the callback is invoked in a spawned task, so it may wait for
301    /// later connection traffic. A response interceptor that retains the response and routes it
302    /// later cannot retroactively order this setup before messages the loop already processed.
303    pub fn on_proxy_session_start<F, Fut>(
304        self,
305        responder: Responder<NewSessionResponse>,
306        op: F,
307    ) -> Result<(), crate::Error>
308    where
309        F: FnOnce(SessionId) -> Fut + Send + 'static,
310        Fut: Future<Output = Result<(), crate::Error>> + Send,
311        Counterpart: HasPeer<Client>,
312        R: 'static,
313    {
314        let Self {
315            connection,
316            request,
317            dynamic_handler_registrations,
318            run,
319            block_state: _,
320        } = self;
321
322        // Send the "new session" request to the agent.
323        let sent = connection.send_request_to(Agent, request);
324        let sent = sent.forward_cancellation_from(responder.cancellation());
325
326        sent.on_receiving_ok_result(responder, {
327            let connection = connection.clone();
328            async move |response, responder| {
329                // Extract the session-id from the response and forward
330                // the response back to the client
331                let session_id = response.session_id.clone();
332                responder.respond(response)?;
333
334                // Install a dynamic handler to proxy messages from this session
335                connection
336                    .add_dynamic_handler(ProxySessionMessages::new(session_id.clone()))?
337                    .detach();
338
339                // Spawn off the run and dynamic handlers to run indefinitely
340                connection.spawn(run.run_with_connection_to(connection.clone()))?;
341                dynamic_handler_registrations
342                    .into_iter()
343                    .for_each(DynamicHandlerGuard::detach);
344
345                connection.spawn(async move { op(session_id).await })
346            }
347        })
348    }
349}
350
351impl<Counterpart, R> SessionBuilder<Counterpart, R, NonBlocking>
352where
353    Counterpart: HasPeer<Agent>,
354    R: RunWithConnectionTo<Counterpart>,
355{
356    /// Mark this session builder as being able to block the current task.
357    ///
358    /// After calling this, you can use [`run_until`](Self::run_until) or
359    /// [`start_session`](Self::start_session) which block the current task.
360    ///
361    /// This should not be used from inside a message handler like
362    /// [`Builder::on_receive_request`](`crate::Builder::on_receive_request`) or [`HandleDispatchFrom`]
363    /// implementations.
364    pub fn block_task(self) -> SessionBuilder<Counterpart, R, Blocking> {
365        SessionBuilder {
366            connection: self.connection,
367            request: self.request,
368            dynamic_handler_registrations: self.dynamic_handler_registrations,
369            run: self.run,
370            block_state: PhantomData,
371        }
372    }
373}
374
375impl<Counterpart, R> SessionBuilder<Counterpart, R, Blocking>
376where
377    Counterpart: HasPeer<Agent>,
378    R: RunWithConnectionTo<Counterpart>,
379{
380    /// Run this session synchronously. The current task will be blocked
381    /// and `op` will be executed with the active session information.
382    /// This is useful when you have MCP servers that are borrowed from your local
383    /// stack frame.
384    ///
385    /// The `ActiveSession` passed to `op` has a non-`'static` lifetime, which
386    /// prevents calling [`ActiveSession::proxy_remaining_messages`] (since the
387    /// session's background runners would terminate when `op` returns).
388    ///
389    /// Requires calling [`block_task`](Self::block_task) first.
390    pub async fn run_until<T>(
391        self,
392        op: impl for<'runner> AsyncFnOnce(
393            ActiveSession<'runner, Counterpart>,
394        ) -> Result<T, crate::Error>,
395    ) -> Result<T, crate::Error> {
396        let Self {
397            connection,
398            request,
399            dynamic_handler_registrations,
400            run,
401            block_state: _,
402        } = self;
403
404        let response = connection
405            .send_request_to(Agent, request)
406            .block_task()
407            .await?;
408
409        let active_session = connection.attach_session(response, dynamic_handler_registrations)?;
410
411        run_until(
412            run.run_with_connection_to(connection.clone()),
413            op(active_session),
414        )
415        .await
416    }
417
418    /// Send the request to create the session and return a handle.
419    /// This is an alternative to [`Self::run_until`] that avoids rightward
420    /// drift but at the cost of requiring MCP servers that are `Send` and
421    /// don't access data from the surrounding scope.
422    ///
423    /// Returns an `ActiveSession<'static, _>` because the session's runners are spawned into
424    /// background tasks that live for the connection lifetime.
425    ///
426    /// Requires calling [`block_task`](Self::block_task) first.
427    pub async fn start_session(self) -> Result<ActiveSession<'static, Counterpart>, crate::Error>
428    where
429        R: 'static,
430    {
431        let Self {
432            connection,
433            request,
434            dynamic_handler_registrations,
435            run,
436            block_state: _,
437        } = self;
438
439        let (active_session_tx, active_session_rx) = oneshot::channel();
440
441        connection.clone().spawn(async move {
442            let response = connection
443                .send_request_to(Agent, request)
444                .block_task()
445                .await?;
446
447            connection.spawn(run.run_with_connection_to(connection.clone()))?;
448
449            let active_session =
450                connection.attach_session(response, dynamic_handler_registrations)?;
451
452            active_session_tx
453                .send(active_session)
454                .map_err(|_| crate::Error::internal_error())?;
455
456            Ok(())
457        })?;
458
459        active_session_rx
460            .await
461            .map_err(|_| crate::Error::internal_error())
462    }
463
464    /// Start a proxy session that forwards all messages between client and agent.
465    ///
466    /// A **proxy session** starts the session with the agent and then automatically
467    /// proxies all session updates (prompts, tool calls, etc.) from the agent back
468    /// to the client. You don't need to handle any messages yourself - the proxy
469    /// takes care of forwarding everything. This is useful when you want to inject
470    /// and/or filter prompts coming from the client but otherwise not be involved
471    /// in the session.
472    ///
473    /// This is a convenience method that combines [`start_session`](Self::start_session),
474    /// responding to the client, and [`ActiveSession::proxy_remaining_messages`].
475    ///
476    /// For more control (e.g., to send some messages before proxying), use
477    /// [`start_session`](Self::start_session) instead and call
478    /// [`proxy_remaining_messages`](ActiveSession::proxy_remaining_messages) manually.
479    ///
480    /// Requires calling [`block_task`](Self::block_task) first.
481    pub async fn start_session_proxy(
482        self,
483        responder: Responder<NewSessionResponse>,
484    ) -> Result<SessionId, crate::Error>
485    where
486        Counterpart: HasPeer<Client>,
487        R: 'static,
488    {
489        let active_session = self.start_session().await?;
490        let session_id = active_session.session_id().clone();
491        responder.respond(active_session.response())?;
492        active_session.proxy_remaining_messages()?;
493        Ok(session_id)
494    }
495}
496
497/// Active session struct that lets you send prompts and receive updates.
498///
499/// The `'runner` lifetime represents the span during which session support runners
500/// (such as MCP servers) are active. When created via [`SessionBuilder::start_session`],
501/// this is `'static` because the runners are spawned into background tasks.
502/// When created via [`SessionBuilder::run_until`], this is tied to the
503/// closure scope, preventing [`Self::proxy_remaining_messages`] from being called
504/// (since the runners would stop when the closure returns).
505#[derive(Debug)]
506pub struct ActiveSession<'runner, Link>
507where
508    Link: HasPeer<Agent>,
509{
510    session_id: SessionId,
511    update_rx: mpsc::UnboundedReceiver<SessionMessage>,
512    update_tx: mpsc::UnboundedSender<SessionMessage>,
513    modes: Option<SessionModeState>,
514    meta: Option<serde_json::Map<String, serde_json::Value>>,
515    connection: ConnectionTo<Link>,
516
517    /// Registration for the handler that routes session messages to `update_rx`.
518    /// This is separate from MCP handlers so it can be dropped independently
519    /// when switching to proxy mode.
520    session_handler_registration: DynamicHandlerGuard<Link>,
521
522    /// Registrations for MCP server handlers.
523    /// These will be dropped once the active-session struct is dropped
524    /// which will cause them to be deregistered.
525    mcp_handler_registrations: Vec<DynamicHandlerGuard<Link>>,
526
527    /// Phantom lifetime representing the session-runner lifetime.
528    _runner: PhantomData<&'runner ()>,
529}
530
531/// Incoming message from the agent
532#[non_exhaustive]
533#[derive(Debug)]
534#[allow(
535    clippy::large_enum_variant,
536    reason = "Dispatch messages vastly outnumber StopReason; boxing would add a heap allocation"
537)]
538pub enum SessionMessage {
539    /// Periodic updates with new content, tool requests, etc.
540    /// Use [`MatchDispatch`] to match on the message type.
541    SessionMessage(Dispatch),
542
543    /// When a prompt completes, the stop reason.
544    StopReason(StopReason),
545}
546
547impl<Link> ActiveSession<'_, Link>
548where
549    Link: HasPeer<Agent>,
550{
551    /// Access the session ID.
552    pub fn session_id(&self) -> &SessionId {
553        &self.session_id
554    }
555
556    /// Access modes available in this session.
557    pub fn modes(&self) -> Option<&SessionModeState> {
558        self.modes.as_ref()
559    }
560
561    /// Access meta data from session response.
562    pub fn meta(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
563        self.meta.as_ref()
564    }
565
566    /// Build a `NewSessionResponse` from the session information.
567    ///
568    /// Useful when you need to forward the session response to a client
569    /// after doing some processing.
570    pub fn response(&self) -> NewSessionResponse {
571        NewSessionResponse::new(self.session_id.clone())
572            .modes(self.modes.clone())
573            .meta(self.meta.clone())
574    }
575
576    /// Access the underlying connection context used to communicate with the agent.
577    pub fn connection(&self) -> &ConnectionTo<Link> {
578        &self.connection
579    }
580
581    /// Send a prompt to the agent. You can then read messages sent in response.
582    pub fn send_prompt(&mut self, prompt: impl ToString) -> Result<(), crate::Error> {
583        let update_tx = self.update_tx.clone();
584        self.connection
585            .send_request_to(
586                Agent,
587                PromptRequest::new(self.session_id.clone(), vec![prompt.to_string().into()]),
588            )
589            .on_receiving_result(async move |result| {
590                let PromptResponse { stop_reason, .. } = result?;
591
592                update_tx
593                    .unbounded_send(SessionMessage::StopReason(stop_reason))
594                    .map_err(crate::util::internal_error)?;
595
596                Ok(())
597            })
598    }
599
600    /// Read an update from the agent in response to the prompt.
601    pub async fn read_update(&mut self) -> Result<SessionMessage, crate::Error> {
602        use futures::StreamExt;
603        let message =
604            self.update_rx.next().await.ok_or_else(|| {
605                crate::util::internal_error("session channel closed unexpectedly")
606            })?;
607
608        Ok(message)
609    }
610
611    /// Read all updates until the end of the turn and create a string.
612    /// Ignores non-text updates.
613    pub async fn read_to_string(&mut self) -> Result<String, crate::Error> {
614        let mut output = String::new();
615        loop {
616            let update = self.read_update().await?;
617            tracing::trace!(?update, "read_to_string update");
618            match update {
619                SessionMessage::SessionMessage(dispatch) => MatchDispatch::new(dispatch)
620                    .if_notification(async |notif: SessionNotification| match notif.update {
621                        SessionUpdate::AgentMessageChunk(ContentChunk {
622                            content: ContentBlock::Text(text),
623                            ..
624                        }) => {
625                            output.push_str(&text.text);
626                            Ok(())
627                        }
628                        _ => Ok(()),
629                    })
630                    .await
631                    .otherwise_ignore()?,
632                SessionMessage::StopReason(_stop_reason) => break,
633            }
634        }
635        Ok(output)
636    }
637}
638
639impl<Link> ActiveSession<'static, Link>
640where
641    Link: HasPeer<Agent>,
642{
643    /// Proxy all remaining messages for this session between client and agent.
644    ///
645    /// Use this when you want to inject MCP servers into a session but don't need
646    /// to actively interact with it after setup. The session messages will be proxied
647    /// between client and agent automatically.
648    ///
649    /// This consumes the `ActiveSession` since you're giving up active control.
650    ///
651    /// This method is only available on `ActiveSession<'static, _>` (from
652    /// [`SessionBuilder::start_session`]) because it requires the session's runners to outlive
653    /// the method call.
654    ///
655    /// # Message Ordering Guarantees
656    ///
657    /// This method ensures proper handoff from active session mode to proxy mode
658    /// without losing or reordering messages:
659    ///
660    /// 1. **Stop the session handler** - Drop the registration that routes messages
661    ///    to `update_rx`. After this, no new messages will be queued.
662    /// 2. **Close the channel** - Drop `update_tx` so we can detect when the channel
663    ///    is fully drained.
664    /// 3. **Drain queued messages** - Forward any messages that were already queued
665    ///    in `update_rx` to the client, preserving order.
666    /// 4. **Install proxy handler** - Now that all queued messages are forwarded,
667    ///    install the proxy handler to handle future messages.
668    ///
669    /// This sequence prevents the race condition where messages could be delivered
670    /// out of order or lost during the transition.
671    pub fn proxy_remaining_messages(self) -> Result<(), crate::Error>
672    where
673        Link: HasPeer<Client>,
674    {
675        // Destructure self to get ownership of all fields
676        let ActiveSession {
677            session_id,
678            mut update_rx,
679            update_tx,
680            connection,
681            session_handler_registration,
682            mcp_handler_registrations,
683            // These fields are not needed for proxying
684            modes: _,
685            meta: _,
686            _runner,
687        } = self;
688
689        // Step 1: Drop the session handler registration.
690        // This unregisters the handler that was routing messages to update_rx.
691        // After this point, no new messages will be added to the channel.
692        drop(session_handler_registration);
693
694        // Step 2: Drop the sender side of the channel.
695        // This allows us to detect when the channel is fully drained
696        // (recv will return None when empty and sender is dropped).
697        drop(update_tx);
698
699        // Step 3: Drain any messages that were already queued and forward to client.
700        // These messages arrived before we dropped the handler but haven't been
701        // consumed yet. We must forward them to maintain message ordering.
702        while let Ok(message) = update_rx.try_recv() {
703            match message {
704                SessionMessage::SessionMessage(dispatch) => {
705                    // Forward the message to the client
706                    connection.send_proxied_message_to(Client, dispatch)?;
707                }
708                SessionMessage::StopReason(_) => {
709                    // StopReason is internal bookkeeping, not forwarded
710                }
711            }
712        }
713
714        // Step 4: Install the proxy handler for future messages.
715        // Now that all queued messages have been forwarded, the proxy handler
716        // can take over. Any new messages will go directly through the proxy.
717        connection
718            .add_dynamic_handler(ProxySessionMessages::new(session_id))?
719            .detach();
720
721        // Keep MCP server handlers alive for the lifetime of the proxy
722        for registration in mcp_handler_registrations {
723            registration.detach();
724        }
725
726        Ok(())
727    }
728}
729
730struct ActiveSessionHandler {
731    session_id: SessionId,
732    update_tx: mpsc::UnboundedSender<SessionMessage>,
733}
734
735impl ActiveSessionHandler {
736    pub fn new(session_id: SessionId, update_tx: mpsc::UnboundedSender<SessionMessage>) -> Self {
737        Self {
738            session_id,
739            update_tx,
740        }
741    }
742}
743
744impl<Counterpart: Role> HandleDispatchFrom<Counterpart> for ActiveSessionHandler
745where
746    Counterpart: HasPeer<Agent>,
747{
748    async fn handle_dispatch_from(
749        &mut self,
750        message: Dispatch,
751        cx: ConnectionTo<Counterpart>,
752    ) -> Result<Handled<Dispatch>, crate::Error> {
753        // If this is a message for our session, grab it.
754        tracing::trace!(
755            ?message,
756            handler_session_id = ?self.session_id,
757            "ActiveSessionHandler::handle_dispatch"
758        );
759        MatchDispatchFrom::new(message, &cx)
760            .if_dispatch_from(Agent, async |message| {
761                if let Some(session_id) = message.get_session_id()? {
762                    tracing::trace!(
763                        message_session_id = ?session_id,
764                        handler_session_id = ?self.session_id,
765                        "ActiveSessionHandler::handle_dispatch"
766                    );
767                    if session_id == self.session_id {
768                        self.update_tx
769                            .unbounded_send(SessionMessage::SessionMessage(message))
770                            .map_err(crate::util::internal_error)?;
771                        return Ok(Handled::Yes);
772                    }
773                }
774
775                // Otherwise, pass it through.
776                Ok(Handled::No {
777                    message,
778                    retry: false,
779                })
780            })
781            .await
782            .done()
783    }
784
785    fn describe_chain(&self) -> impl std::fmt::Debug {
786        format!("ActiveSessionHandler({})", self.session_id)
787    }
788}