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}