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}