Skip to main content

agent_client_protocol/session/
v2.rs

1use std::{future::Future, path::Path};
2
3use futures::{
4    channel::oneshot,
5    future::{self, Either},
6};
7
8use crate::{
9    Agent, Client, ConnectionTo as RawConnectionTo, DynamicHandlerGuard, JsonRpcRequest, Responder,
10    SentRequest, V2,
11    jsonrpc::run::{NullRun, RunWithConnectionTo},
12    role::{HasPeer, acp::ProxySessionMessages},
13    v2::ConnectionTo,
14    v2::schema as v2,
15};
16
17#[cfg(feature = "unstable_mcp_over_acp")]
18use crate::{jsonrpc::run::ChainRun, mcp_server::McpServer};
19
20use super::{
21    SessionCleanup, close_session_registrations, drive_session_cleanup,
22    run_attached_session_runner, session_cleanup, session_runner_connection, wait_session_cleanup,
23};
24
25async fn run_pending_session_setup<Counterpart, Run>(
26    connection: RawConnectionTo<Counterpart>,
27    run: Run,
28    started_tx: oneshot::Sender<Result<(), crate::Error>>,
29    promotion_rx: oneshot::Receiver<()>,
30    cleanup: SessionCleanup,
31) -> Result<(), crate::Error>
32where
33    Counterpart: HasPeer<Agent>,
34    Run: RunWithConnectionTo<Counterpart>,
35{
36    let (connection, error_scope) = session_runner_connection(connection, &cleanup);
37    let mut run = Box::pin(run.run_with_connection_to(connection));
38    let first_poll =
39        future::poll_fn(|cx| std::task::Poll::Ready(std::future::Future::poll(run.as_mut(), cx)))
40            .await;
41    // A composed runner may have observed an immediate error but remain Pending
42    // while its sibling drives owned cleanup. It still failed before publication.
43    if let Some(error) = error_scope.error() {
44        close_session_registrations(&cleanup);
45        if first_poll.is_pending() {
46            drop(drive_session_cleanup(run, cleanup).await);
47        } else {
48            wait_session_cleanup(cleanup).await;
49        }
50        drop(started_tx.send(Err(error)));
51        return Ok(());
52    }
53    let readiness = match &first_poll {
54        std::task::Poll::Ready(result) => result.clone(),
55        std::task::Poll::Pending => Ok(()),
56    };
57    drop(started_tx.send(readiness));
58
59    match first_poll {
60        std::task::Poll::Ready(Ok(())) => {
61            if promotion_rx.await.is_err() {
62                close_session_registrations(&cleanup);
63                wait_session_cleanup(cleanup).await;
64            }
65            Ok(())
66        }
67        // The request has not been published yet, so report an immediate
68        // startup failure through its readiness result without failing the
69        // whole connection.
70        std::task::Poll::Ready(Err(_)) => {
71            close_session_registrations(&cleanup);
72            wait_session_cleanup(cleanup).await;
73            Ok(())
74        }
75        std::task::Poll::Pending => match future::select(run, promotion_rx).await {
76            Either::Left((result, promotion_rx)) => {
77                // A Pending first poll releases session setup for publication.
78                // From that point onward the agent may already be using an
79                // attachment, so runner failures are connection-fatal just as
80                // they are for other connection runners.
81                if result.is_err() || promotion_rx.await.is_err() {
82                    close_session_registrations(&cleanup);
83                    wait_session_cleanup(cleanup).await;
84                }
85                result
86            }
87            Either::Right((Ok(()), run)) => {
88                run_attached_session_runner(run, cleanup, error_scope).await
89            }
90            Either::Right((Err(_), run)) => {
91                close_session_registrations(&cleanup);
92                run_attached_session_runner(run, cleanup, error_scope).await
93            }
94        },
95    }
96}
97
98fn send_session_setup<Counterpart, Request, Run>(
99    connection: ConnectionTo<Counterpart>,
100    request: Request,
101    dynamic_handler_registrations: Vec<DynamicHandlerGuard<Counterpart>>,
102    run: Run,
103    ordered: bool,
104) -> SentRequest<Request::Response, V2>
105where
106    Counterpart: HasPeer<Agent>,
107    Request: JsonRpcRequest + 'static,
108    Request::Response: 'static,
109    Run: RunWithConnectionTo<Counterpart> + 'static,
110{
111    let raw_connection = connection.raw_connection().clone();
112    if dynamic_handler_registrations.is_empty() {
113        drop(run);
114        if ordered {
115            raw_connection.send_ordered_request_to(Agent, request)
116        } else {
117            connection.send_request_to(Agent, request)
118        }
119    } else {
120        let handlers_ready = raw_connection.dynamic_handler_barrier();
121        let (runner_started_tx, runner_started_rx) = oneshot::channel();
122        let (promotion_tx, promotion_rx) = oneshot::channel();
123        let cleanup = session_cleanup(&dynamic_handler_registrations);
124        let runner_started = match raw_connection.spawn(run_pending_session_setup(
125            raw_connection.clone(),
126            run,
127            runner_started_tx,
128            promotion_rx,
129            cleanup,
130        )) {
131            Ok(()) => Either::Left(async move {
132                runner_started_rx.await.map_err(|error| {
133                    crate::util::internal_error(format!(
134                        "session setup runner stopped before its initial poll: {error}"
135                    ))
136                })?
137            }),
138            Err(error) => Either::Right(future::ready(Err(error))),
139        };
140        let readiness = async move {
141            future::try_join(handlers_ready, runner_started).await?;
142            Ok(())
143        };
144        let response_hook = move |_response: &Request::Response| {
145            promotion_tx.send(()).map_err(|()| {
146                crate::util::internal_error("session setup runner stopped before setup completed")
147            })?;
148            dynamic_handler_registrations
149                .into_iter()
150                .for_each(DynamicHandlerGuard::detach);
151            Ok(())
152        };
153
154        if ordered {
155            raw_connection.send_ordered_request_to_with_response_hook_after(
156                Agent,
157                request,
158                readiness,
159                response_hook,
160            )
161        } else {
162            raw_connection.send_request_to_with_response_hook_after(
163                Agent,
164                request,
165                readiness,
166                response_hook,
167            )
168        }
169    }
170}
171
172impl<Counterpart> ConnectionTo<Counterpart>
173where
174    Counterpart: HasPeer<Agent>,
175{
176    /// Build a draft protocol v2 `session/new` request.
177    ///
178    /// `cwd` must be an absolute path: ACP requires one, and the SDK sends it
179    /// without checking.
180    pub fn build_session(&self, cwd: impl AsRef<Path>) -> SessionBuilder<Counterpart> {
181        SessionBuilder::new(self, v2::NewSessionRequest::new(cwd.as_ref()))
182    }
183
184    /// Build a draft protocol v2 session using the current working directory.
185    ///
186    /// Returns an error if the current directory cannot be determined.
187    pub fn build_session_cwd(&self) -> Result<SessionBuilder<Counterpart>, crate::Error> {
188        let cwd = std::env::current_dir().map_err(|error| {
189            crate::Error::internal_error().data(format!("cannot get current directory: {error}"))
190        })?;
191        Ok(self.build_session(cwd))
192    }
193
194    /// Build a draft protocol v2 session from an existing `session/new` request.
195    pub fn build_session_from(
196        &self,
197        request: v2::NewSessionRequest,
198    ) -> SessionBuilder<Counterpart> {
199        SessionBuilder::new(self, request)
200    }
201
202    /// Build an unstable draft protocol v2 `session/fork` request.
203    ///
204    /// This helper is available with the `unstable_session_fork` feature. Call
205    /// [`ForkSessionBuilder::start_session`] to publish the request and
206    /// obtain a command handle for the newly created fork.
207    ///
208    /// `cwd` must be an absolute path: ACP requires one, and the SDK sends it
209    /// without checking.
210    #[cfg(feature = "unstable_session_fork")]
211    pub fn fork_session(
212        &self,
213        session_id: impl Into<v2::SessionId>,
214        cwd: impl AsRef<Path>,
215    ) -> ForkSessionBuilder<Counterpart> {
216        self.fork_session_from(v2::ForkSessionRequest::new(session_id, cwd.as_ref()))
217    }
218
219    /// Build an unstable draft protocol v2 `session/fork` request from an
220    /// existing request.
221    ///
222    /// This helper is available with the `unstable_session_fork` feature. Call
223    /// [`ForkSessionBuilder::start_session`] to publish the request.
224    #[cfg(feature = "unstable_session_fork")]
225    pub fn fork_session_from(
226        &self,
227        request: v2::ForkSessionRequest,
228    ) -> ForkSessionBuilder<Counterpart> {
229        ForkSessionBuilder::new(self, request)
230    }
231
232    /// Build a draft protocol v2 `session/resume` request.
233    ///
234    /// Use [`Self::resume_session_from`] to request history replay or set
235    /// other optional resume parameters. Call
236    /// [`ResumeSessionBuilder::start_session`] to publish the request.
237    ///
238    /// `cwd` must be an absolute path: ACP requires one, and the SDK sends it
239    /// without checking.
240    pub fn resume_session(
241        &self,
242        session_id: impl Into<v2::SessionId>,
243        cwd: impl AsRef<Path>,
244    ) -> ResumeSessionBuilder<Counterpart> {
245        self.resume_session_from(v2::ResumeSessionRequest::new(session_id, cwd.as_ref()))
246    }
247
248    /// Build a draft protocol v2 `session/resume` request from an existing request.
249    ///
250    /// Register typed session update handlers before connecting. When the
251    /// request asks for replay, the agent sends those updates before the
252    /// [`v2::schema::ResumeSessionResponse`](v2::ResumeSessionResponse). Call
253    /// [`ResumeSessionBuilder::start_session`] to publish the request.
254    pub fn resume_session_from(
255        &self,
256        request: v2::ResumeSessionRequest,
257    ) -> ResumeSessionBuilder<Counterpart> {
258        ResumeSessionBuilder::new(self, request)
259    }
260}
261
262/// Builder for a draft protocol v2 `session/new` request.
263///
264/// Protocol v2 acknowledges `session/prompt` independently from inbound
265/// session updates. Register typed [`v2::schema::UpdateSessionNotification`](v2::UpdateSessionNotification) and
266/// session request handlers on [`v2::Builder`](crate::v2::Builder) before connecting, then use
267/// [`Self::start_session`] to create the command-only [`ActiveSession`] handle or
268/// `on_proxy_session_start` to forward setup through a proxy.
269///
270/// With both the `unstable_protocol_v2` and `unstable_mcp_over_acp` features,
271/// `with_mcp_server` attaches an MCP server to the new session.
272#[must_use = "call `start_session` or `on_proxy_session_start` to send the `session/new` request"]
273#[derive(Debug)]
274pub struct SessionBuilder<Counterpart, Run = NullRun>
275where
276    Counterpart: HasPeer<Agent>,
277    Run: RunWithConnectionTo<Counterpart>,
278{
279    connection: ConnectionTo<Counterpart>,
280    request: v2::NewSessionRequest,
281    dynamic_handler_registrations: Vec<DynamicHandlerGuard<Counterpart>>,
282    run: Run,
283}
284
285impl<Counterpart> SessionBuilder<Counterpart, NullRun>
286where
287    Counterpart: HasPeer<Agent>,
288{
289    fn new(connection: &ConnectionTo<Counterpart>, request: v2::NewSessionRequest) -> Self {
290        Self {
291            connection: connection.clone(),
292            request,
293            dynamic_handler_registrations: Vec::new(),
294            run: NullRun,
295        }
296    }
297}
298
299impl<Counterpart, Run> SessionBuilder<Counterpart, Run>
300where
301    Counterpart: HasPeer<Agent>,
302    Run: RunWithConnectionTo<Counterpart>,
303{
304    /// Attach an MCP server to this new protocol v2 session.
305    ///
306    /// This method is available when both `unstable_protocol_v2` and
307    /// `unstable_mcp_over_acp` are enabled. MCP routes are installed and their
308    /// runner tasks receive an initial poll before `session/new` is published,
309    /// allowing the agent to connect while handling session setup. A
310    /// successful attachment remains active for the lifetime of the connection.
311    ///
312    /// The declaration is an `acp` transport (`McpServer::Acp`). The agent must
313    /// advertise `session.mcp.acp`, or a compatibility proxy such as
314    /// `McpOverAcpPolyfill` must sit in front of it; otherwise a conforming
315    /// agent rejects the setup request with `-32602`. This method does not
316    /// check the agent's capabilities.
317    #[cfg(feature = "unstable_mcp_over_acp")]
318    pub fn with_mcp_server<McpRun>(
319        mut self,
320        mcp_server: McpServer<Counterpart, McpRun>,
321    ) -> Result<SessionBuilder<Counterpart, ChainRun<Run, McpRun>>, crate::Error>
322    where
323        McpRun: RunWithConnectionTo<Counterpart>,
324    {
325        let (handler, mcp_run) = mcp_server.into_v2_handler_and_runner();
326        self.dynamic_handler_registrations
327            .push(handler.into_dynamic_handler(&mut self.request.mcp_servers, &self.connection)?);
328        Ok(SessionBuilder {
329            connection: self.connection,
330            request: self.request,
331            dynamic_handler_registrations: self.dynamic_handler_registrations,
332            run: ChainRun::new(self.run, mcp_run),
333        })
334    }
335
336    fn send_new_session(self, ordered: bool) -> SentRequest<v2::NewSessionResponse, V2>
337    where
338        Run: 'static,
339    {
340        let Self {
341            connection,
342            request,
343            dynamic_handler_registrations,
344            run,
345        } = self;
346        send_session_setup(
347            connection,
348            request,
349            dynamic_handler_registrations,
350            run,
351            ordered,
352        )
353    }
354
355    /// Send `session/new` and return its independently consumable request.
356    ///
357    /// The successful result contains both a cloneable command handle and the
358    /// complete [`v2::schema::NewSessionResponse`](v2::NewSessionResponse). Consume the returned request with
359    /// [`SentRequest::block_task`], [`SentRequest::on_receiving_result`], or
360    /// another explicit [`SentRequest`] completion mode.
361    ///
362    /// Attached MCP routes are installed and their runner tasks begin
363    /// executing before the request is published. A valid success response
364    /// promotes them to the connection lifetime, independently from how this
365    /// request handle is consumed. Setup errors clean up the pending
366    /// attachment.
367    pub fn start_session(self) -> SentRequest<SessionSetup<Counterpart, v2::NewSessionResponse>, V2>
368    where
369        Run: 'static,
370    {
371        let session_connection = self.connection.clone();
372        self.send_new_session(false).map(move |response| {
373            let session = ActiveSession {
374                session_id: response.session_id.clone(),
375                connection: session_connection,
376            };
377            Ok(SessionSetup { session, response })
378        })
379    }
380
381    /// Start a protocol v2 session through a proxy and forward its response.
382    ///
383    /// The downstream request is ordered and inherits cancellation from the
384    /// upstream request. On success, this helper installs session routing before
385    /// later inbound traffic is processed, forwards the complete response, and
386    /// spawns `op` with an [`SessionSetup`] containing the command-only
387    /// session handle plus the complete setup response. Inbound updates and
388    /// interactive requests remain independent connection traffic.
389    ///
390    /// The callback runs outside the ordered response barrier, so it may wait
391    /// for later connection traffic without deadlocking the dispatch loop.
392    pub fn on_proxy_session_start<F, Fut>(
393        self,
394        responder: Responder<v2::NewSessionResponse, V2>,
395        op: F,
396    ) -> Result<(), crate::Error>
397    where
398        Counterpart: HasPeer<Client>,
399        Run: 'static,
400        F: FnOnce(SessionSetup<Counterpart, v2::NewSessionResponse>) -> Fut + Send + 'static,
401        Fut: Future<Output = Result<(), crate::Error>> + Send,
402    {
403        let session_connection = self.connection.clone();
404        self.send_new_session(true)
405            .forward_cancellation_from(responder.cancellation())
406            .on_receiving_ok_result(responder, async move |response, responder| {
407                let session_id = response.session_id.clone();
408                let raw_connection = session_connection.raw_connection();
409                let route = match raw_connection.add_dynamic_handler(ProxySessionMessages::new(
410                    crate::v1::schema::SessionId::from(session_id.0.clone()),
411                )) {
412                    Ok(route) => route,
413                    Err(error) => return responder.respond_with_error(error),
414                };
415
416                let setup = SessionSetup {
417                    session: ActiveSession {
418                        session_id,
419                        connection: session_connection.clone(),
420                    },
421                    response: response.clone(),
422                };
423                responder.respond(response)?;
424                route.detach();
425                raw_connection.spawn(async move { op(setup).await })
426            })
427    }
428}
429
430/// Builder for an unstable draft protocol v2 `session/fork` request.
431///
432/// A successful fork creates a new independent session whose ID comes from the
433/// [`v2::schema::ForkSessionResponse`](v2::ForkSessionResponse). Register typed
434/// [`v2::schema::UpdateSessionNotification`](v2::UpdateSessionNotification) and interactive request handlers on
435/// [`v2::Builder`](crate::v2::Builder) before connecting, then use [`Self::start_session`] to
436/// obtain a command handle for the fork or `on_proxy_session_start` to forward
437/// setup through a proxy.
438///
439/// This type is available with the `unstable_session_fork` feature. With
440/// `unstable_mcp_over_acp` as well, `with_mcp_server` attaches an MCP server to
441/// the forked session.
442#[cfg(feature = "unstable_session_fork")]
443#[must_use = "call `start_session` or `on_proxy_session_start` to send the `session/fork` request"]
444#[derive(Debug)]
445pub struct ForkSessionBuilder<Counterpart, Run = NullRun>
446where
447    Counterpart: HasPeer<Agent>,
448    Run: RunWithConnectionTo<Counterpart>,
449{
450    connection: ConnectionTo<Counterpart>,
451    request: v2::ForkSessionRequest,
452    dynamic_handler_registrations: Vec<DynamicHandlerGuard<Counterpart>>,
453    run: Run,
454}
455
456#[cfg(feature = "unstable_session_fork")]
457impl<Counterpart> ForkSessionBuilder<Counterpart, NullRun>
458where
459    Counterpart: HasPeer<Agent>,
460{
461    fn new(connection: &ConnectionTo<Counterpart>, request: v2::ForkSessionRequest) -> Self {
462        Self {
463            connection: connection.clone(),
464            request,
465            dynamic_handler_registrations: Vec::new(),
466            run: NullRun,
467        }
468    }
469}
470
471#[cfg(feature = "unstable_session_fork")]
472impl<Counterpart, Run> ForkSessionBuilder<Counterpart, Run>
473where
474    Counterpart: HasPeer<Agent>,
475    Run: RunWithConnectionTo<Counterpart>,
476{
477    /// Attach an MCP server to this forked protocol v2 session.
478    ///
479    /// This method is available when `unstable_mcp_over_acp` is enabled in
480    /// addition to `unstable_protocol_v2` and `unstable_session_fork`. MCP
481    /// routes are installed and their runner tasks receive an initial poll
482    /// before `session/fork` is published, allowing the agent to connect while
483    /// handling session setup. A successful attachment remains active for the
484    /// lifetime of the connection.
485    ///
486    /// The declaration is an `acp` transport (`McpServer::Acp`). The agent must
487    /// advertise `session.mcp.acp`, or a compatibility proxy such as
488    /// `McpOverAcpPolyfill` must sit in front of it; otherwise a conforming
489    /// agent rejects the setup request with `-32602`. This method does not
490    /// check the agent's capabilities.
491    #[cfg(feature = "unstable_mcp_over_acp")]
492    pub fn with_mcp_server<McpRun>(
493        mut self,
494        mcp_server: McpServer<Counterpart, McpRun>,
495    ) -> Result<ForkSessionBuilder<Counterpart, ChainRun<Run, McpRun>>, crate::Error>
496    where
497        McpRun: RunWithConnectionTo<Counterpart>,
498    {
499        let (handler, mcp_run) = mcp_server.into_v2_handler_and_runner();
500        self.dynamic_handler_registrations
501            .push(handler.into_dynamic_handler(&mut self.request.mcp_servers, &self.connection)?);
502        Ok(ForkSessionBuilder {
503            connection: self.connection,
504            request: self.request,
505            dynamic_handler_registrations: self.dynamic_handler_registrations,
506            run: ChainRun::new(self.run, mcp_run),
507        })
508    }
509
510    fn send_fork_session(self, ordered: bool) -> SentRequest<v2::ForkSessionResponse, V2>
511    where
512        Run: 'static,
513    {
514        let Self {
515            connection,
516            request,
517            dynamic_handler_registrations,
518            run,
519        } = self;
520        send_session_setup(
521            connection,
522            request,
523            dynamic_handler_registrations,
524            run,
525            ordered,
526        )
527    }
528
529    /// Send `session/fork` and return its independently consumable request.
530    ///
531    /// The successful result contains both a cloneable command handle for the
532    /// newly created fork and the complete [`v2::schema::ForkSessionResponse`](v2::ForkSessionResponse). Consume
533    /// the returned request with [`SentRequest::block_task`],
534    /// [`SentRequest::on_receiving_result`], or another explicit [`SentRequest`]
535    /// completion mode.
536    ///
537    /// Attached MCP routes are installed and their runner tasks begin
538    /// executing before the request is published. A valid success response
539    /// promotes them to the connection lifetime independently from how this
540    /// request handle is consumed; setup errors clean up the pending
541    /// attachment.
542    pub fn start_session(
543        self,
544    ) -> SentRequest<SessionSetup<Counterpart, v2::ForkSessionResponse>, V2>
545    where
546        Run: 'static,
547    {
548        let session_connection = self.connection.clone();
549        self.send_fork_session(false).map(move |response| {
550            let session = ActiveSession {
551                session_id: response.session_id.clone(),
552                connection: session_connection,
553            };
554            Ok(SessionSetup { session, response })
555        })
556    }
557
558    /// Fork a protocol v2 session through a proxy and forward its response.
559    ///
560    /// The downstream request is ordered and inherits cancellation from the
561    /// upstream request. On success, this helper obtains the new session ID
562    /// from the response, installs session routing before later inbound traffic
563    /// is processed, forwards the complete response, and spawns `op` with an
564    /// [`SessionSetup`] containing the fork's command handle and response.
565    /// Inbound updates and interactive requests remain independent connection
566    /// traffic.
567    ///
568    /// The callback runs outside the ordered response barrier, so it may wait
569    /// for later connection traffic without deadlocking the dispatch loop.
570    pub fn on_proxy_session_start<F, Fut>(
571        self,
572        responder: Responder<v2::ForkSessionResponse, V2>,
573        op: F,
574    ) -> Result<(), crate::Error>
575    where
576        Counterpart: HasPeer<Client>,
577        Run: 'static,
578        F: FnOnce(SessionSetup<Counterpart, v2::ForkSessionResponse>) -> Fut + Send + 'static,
579        Fut: Future<Output = Result<(), crate::Error>> + Send,
580    {
581        let session_connection = self.connection.clone();
582        self.send_fork_session(true)
583            .forward_cancellation_from(responder.cancellation())
584            .on_receiving_ok_result(responder, async move |response, responder| {
585                let session_id = response.session_id.clone();
586                let raw_connection = session_connection.raw_connection();
587                let route = match raw_connection.add_dynamic_handler(ProxySessionMessages::new(
588                    crate::v1::schema::SessionId::from(session_id.0.clone()),
589                )) {
590                    Ok(route) => route,
591                    Err(error) => return responder.respond_with_error(error),
592                };
593
594                let setup = SessionSetup {
595                    session: ActiveSession {
596                        session_id,
597                        connection: session_connection.clone(),
598                    },
599                    response: response.clone(),
600                };
601                responder.respond(response)?;
602                route.detach();
603                raw_connection.spawn(async move { op(setup).await })
604            })
605    }
606}
607
608/// Builder for a draft protocol v2 `session/resume` request.
609///
610/// Replay updates arrive before the resume response. Direct clients must
611/// register typed [`v2::schema::UpdateSessionNotification`](v2::UpdateSessionNotification) and interactive request
612/// handlers on [`v2::Builder`](crate::v2::Builder) before connecting. Proxies should use
613/// [`Self::on_proxy_session_start`], which makes downstream session routing
614/// ready before publishing the resume request.
615///
616/// With both the `unstable_protocol_v2` and `unstable_mcp_over_acp` features,
617/// `with_mcp_server` attaches an MCP server to the resumed session and makes it
618/// ready before the agent can send replay or its response.
619#[must_use = "call `start_session` or `on_proxy_session_start` to send the `session/resume` request"]
620#[derive(Debug)]
621pub struct ResumeSessionBuilder<Counterpart, Run = NullRun>
622where
623    Counterpart: HasPeer<Agent>,
624    Run: RunWithConnectionTo<Counterpart>,
625{
626    connection: ConnectionTo<Counterpart>,
627    request: v2::ResumeSessionRequest,
628    dynamic_handler_registrations: Vec<DynamicHandlerGuard<Counterpart>>,
629    run: Run,
630}
631
632impl<Counterpart> ResumeSessionBuilder<Counterpart, NullRun>
633where
634    Counterpart: HasPeer<Agent>,
635{
636    fn new(connection: &ConnectionTo<Counterpart>, request: v2::ResumeSessionRequest) -> Self {
637        Self {
638            connection: connection.clone(),
639            request,
640            dynamic_handler_registrations: Vec::new(),
641            run: NullRun,
642        }
643    }
644}
645
646impl<Counterpart, Run> ResumeSessionBuilder<Counterpart, Run>
647where
648    Counterpart: HasPeer<Agent>,
649    Run: RunWithConnectionTo<Counterpart>,
650{
651    /// Attach an MCP server to this resumed protocol v2 session.
652    ///
653    /// This method is available when both `unstable_protocol_v2` and
654    /// `unstable_mcp_over_acp` are enabled. MCP routes are installed and their
655    /// runner tasks receive an initial poll before `session/resume` is
656    /// published, allowing the agent to use the server during replay and
657    /// session setup. A successful attachment remains active for the lifetime
658    /// of the connection.
659    ///
660    /// The declaration is an `acp` transport (`McpServer::Acp`). The agent must
661    /// advertise `session.mcp.acp`, or a compatibility proxy such as
662    /// `McpOverAcpPolyfill` must sit in front of it; otherwise a conforming
663    /// agent rejects the setup request with `-32602`. This method does not
664    /// check the agent's capabilities.
665    #[cfg(feature = "unstable_mcp_over_acp")]
666    pub fn with_mcp_server<McpRun>(
667        mut self,
668        mcp_server: McpServer<Counterpart, McpRun>,
669    ) -> Result<ResumeSessionBuilder<Counterpart, ChainRun<Run, McpRun>>, crate::Error>
670    where
671        McpRun: RunWithConnectionTo<Counterpart>,
672    {
673        let (handler, mcp_run) = mcp_server.into_v2_handler_and_runner();
674        self.dynamic_handler_registrations
675            .push(handler.into_dynamic_handler(&mut self.request.mcp_servers, &self.connection)?);
676        Ok(ResumeSessionBuilder {
677            connection: self.connection,
678            request: self.request,
679            dynamic_handler_registrations: self.dynamic_handler_registrations,
680            run: ChainRun::new(self.run, mcp_run),
681        })
682    }
683
684    fn send_resume_session(self, ordered: bool) -> SentRequest<v2::ResumeSessionResponse, V2>
685    where
686        Run: 'static,
687    {
688        let Self {
689            connection,
690            request,
691            dynamic_handler_registrations,
692            run,
693        } = self;
694        send_session_setup(
695            connection,
696            request,
697            dynamic_handler_registrations,
698            run,
699            ordered,
700        )
701    }
702
703    /// Send `session/resume` and return its independently consumable request.
704    ///
705    /// The successful result contains both a cloneable command handle and the
706    /// complete [`v2::schema::ResumeSessionResponse`](v2::ResumeSessionResponse). Consume the returned request
707    /// with [`SentRequest::block_task`], [`SentRequest::on_receiving_result`],
708    /// or another explicit [`SentRequest`] completion mode.
709    ///
710    /// Replay is delivered through the typed connection handlers before the
711    /// response. Attached MCP routes and runner tasks are ready before the
712    /// request is published. A valid success response promotes them to the
713    /// connection lifetime independently from how this request handle is
714    /// consumed; setup errors clean up the pending attachment.
715    pub fn start_session(
716        self,
717    ) -> SentRequest<SessionSetup<Counterpart, v2::ResumeSessionResponse>, V2>
718    where
719        Run: 'static,
720    {
721        let session_id = self.request.session_id.clone();
722        let session_connection = self.connection.clone();
723        self.send_resume_session(false).map(move |response| {
724            let session = ActiveSession {
725                session_id,
726                connection: session_connection,
727            };
728            Ok(SessionSetup { session, response })
729        })
730    }
731
732    /// Resume a protocol v2 session through a proxy and forward its response.
733    ///
734    /// The session ID is known before the request, so this helper installs and
735    /// acknowledges downstream session routing before publishing the ordered
736    /// `session/resume` request. Replay can therefore be forwarded before the
737    /// response as required by the protocol. The downstream request inherits
738    /// cancellation from the upstream request.
739    ///
740    /// On success, the helper forwards the complete response and spawns `op`
741    /// with an [`SessionSetup`] containing the command-only session handle
742    /// plus that response. The callback runs outside the ordered response
743    /// barrier, so it may wait for later connection traffic without
744    /// deadlocking the dispatch loop.
745    pub fn on_proxy_session_start<F, Fut>(
746        mut self,
747        responder: Responder<v2::ResumeSessionResponse, V2>,
748        op: F,
749    ) -> Result<(), crate::Error>
750    where
751        Counterpart: HasPeer<Client>,
752        Run: 'static,
753        F: FnOnce(SessionSetup<Counterpart, v2::ResumeSessionResponse>) -> Fut + Send + 'static,
754        Fut: Future<Output = Result<(), crate::Error>> + Send,
755    {
756        let session_id = self.request.session_id.clone();
757        let session_connection = self.connection.clone();
758        self.dynamic_handler_registrations.push(
759            session_connection
760                .raw_connection()
761                .add_dynamic_handler(ProxySessionMessages::new(
762                    crate::v1::schema::SessionId::from(session_id.0.clone()),
763                ))?,
764        );
765
766        self.send_resume_session(true)
767            .forward_cancellation_from(responder.cancellation())
768            .on_receiving_ok_result(responder, async move |response, responder| {
769                let setup = SessionSetup {
770                    session: ActiveSession {
771                        session_id,
772                        connection: session_connection.clone(),
773                    },
774                    response: response.clone(),
775                };
776                responder.respond(response)?;
777                session_connection
778                    .raw_connection()
779                    .spawn(async move { op(setup).await })
780            })
781    }
782}
783
784/// A protocol v2 session and the response of the request that set it up.
785///
786/// Every setup method returns one: [`SessionBuilder::start_session`]
787/// (`session/new`), [`ResumeSessionBuilder::start_session`]
788/// (`session/resume`) and, with `unstable_session_fork`,
789/// `ForkSessionBuilder::start_session` (`session/fork`), and their
790/// `on_proxy_session_start` callbacks receive one too. The response is the
791/// complete, operation-specific setup response; it is kept next to the
792/// session rather than inside [`ActiveSession`], so the setup response lives
793/// in the same place for every setup method.
794///
795/// New fields may be added, so destructure with `..`:
796///
797/// ```ignore
798/// let SessionSetup { session, response, .. } = setup;
799/// ```
800#[non_exhaustive]
801#[derive(Debug)]
802pub struct SessionSetup<Link, Response>
803where
804    Link: HasPeer<Agent>,
805{
806    /// The command handle for the session.
807    pub session: ActiveSession<Link>,
808    /// The complete response of the request that set the session up.
809    pub response: Response,
810}
811
812/// Cloneable command handle for a draft protocol v2 session.
813///
814/// Inbound protocol traffic is intentionally not owned by this value. Receive
815/// authoritative [`v2::schema::UpdateSessionNotification`](v2::UpdateSessionNotification) values and interactive
816/// requests such as [`v2::schema::RequestPermissionRequest`](v2::RequestPermissionRequest) through typed handlers
817/// installed on [`v2::Builder`](crate::v2::Builder).
818#[derive(Debug, Clone)]
819pub struct ActiveSession<Link>
820where
821    Link: HasPeer<Agent>,
822{
823    session_id: v2::SessionId,
824    connection: ConnectionTo<Link>,
825}
826
827impl<Link> ActiveSession<Link>
828where
829    Link: HasPeer<Agent>,
830{
831    /// Access the session ID.
832    pub fn session_id(&self) -> &v2::SessionId {
833        &self.session_id
834    }
835
836    /// Access the underlying connection.
837    pub fn connection(&self) -> &ConnectionTo<Link> {
838        &self.connection
839    }
840
841    /// Submit a text prompt and return its independent acceptance request.
842    ///
843    /// A successful response only acknowledges that the agent accepted the
844    /// prompt. The accepted user message, output, state changes, and completion
845    /// arrive independently through [`v2::schema::UpdateSessionNotification`](v2::UpdateSessionNotification).
846    pub fn send_prompt(&self, prompt: impl ToString) -> SentRequest<v2::PromptResponse, V2> {
847        self.send_prompt_blocks(vec![prompt.to_string().into()])
848    }
849
850    /// Submit arbitrary prompt content and return its acceptance request.
851    ///
852    /// `prompt` must not be empty; an agent may reject `[]` with `-32602`.
853    ///
854    /// The SDK does not track foreground state or gate prompt submission
855    /// locally. A prompt may be sent in any state, including while foreground
856    /// work is active: the agent either inserts it right away or holds it and
857    /// inserts held prompts in received order, and it must not reject a prompt
858    /// only because work is active. A held prompt's response stays pending
859    /// until it is inserted. Cancel the returned [`SentRequest`] to withdraw a
860    /// held prompt; [`Self::cancel_active_work`] and [`Self::close`] also
861    /// answer prompts that were not yet inserted with `-32800`.
862    pub fn send_prompt_blocks(
863        &self,
864        prompt: Vec<v2::ContentBlock>,
865    ) -> SentRequest<v2::PromptResponse, V2> {
866        self.connection.send_request_to(
867            Agent,
868            v2::PromptRequest::new(self.session_id.clone(), prompt),
869        )
870    }
871
872    /// Ask the agent to cancel the session's current foreground work.
873    ///
874    /// This is independent from cancelling a prompt's [`SentRequest`].
875    /// `session/cancel` covers the session's foreground work and prompts
876    /// received before it that the agent has not yet inserted; those prompt
877    /// requests fail with `-32800` (request cancelled). A prompt sent after
878    /// this notification is not cancelled by it. A prompt whose user message was already
879    /// inserted still succeeds with its `messageId`. Work in the latest
880    /// `background` list is not covered and keeps running.
881    ///
882    /// If foreground work was active, cancellation completes when the agent
883    /// reports an `idle` state update with [`v2::schema::StopReason::Cancelled`](v2::StopReason::Cancelled); no
884    /// completion update is required when nothing was running. `cancelled` is only
885    /// reported for work the client cancelled, through this notification or
886    /// by closing the session. The client should immediately mark unfinished
887    /// tool calls for the active work as cancelled.
888    ///
889    /// The agent stops waiting for pending
890    /// [`v2::schema::RequestPermissionRequest`](v2::RequestPermissionRequest)s
891    /// of the cancelled work and should cancel each of them with
892    /// `$/cancel_request`. Answer a cancelled permission request with the
893    /// `cancelled` outcome. A client may also answer a request it knows belongs
894    /// to the cancelled work that way without waiting for `$/cancel_request`.
895    ///
896    /// See <https://agentclientprotocol.com/protocol/v2/prompt-lifecycle#cancellation>.
897    pub fn cancel_active_work(&self) -> Result<(), crate::Error> {
898        self.connection.send_notification_to(
899            Agent,
900            v2::CancelSessionNotification::new(self.session_id.clone()),
901        )
902    }
903
904    /// Set a session configuration option.
905    ///
906    /// Only send a `config_id` the agent advertised for this session, with a
907    /// value from that option. The agent rejects an unknown `config_id` or an
908    /// invalid value with `-32602` and leaves the configuration unchanged.
909    ///
910    /// The response contains the complete option set as of the response and
911    /// may report a different `currentValue` than requested; `currentValue`
912    /// is always the value the agent is using. Apply responses and
913    /// `config_option_update` notifications in the order received, each
914    /// replacing the whole list. A duplicate `config_option_update`
915    /// notification for the same change may arrive before the response. The
916    /// response is not cached on this command handle.
917    ///
918    /// See <https://agentclientprotocol.com/protocol/v2/session-config-options>.
919    pub fn set_config_option(
920        &self,
921        config_id: impl Into<v2::SessionConfigId>,
922        value: impl Into<v2::SessionConfigOptionValue>,
923    ) -> SentRequest<v2::SetSessionConfigOptionResponse, V2> {
924        self.connection.send_request_to(
925            Agent,
926            v2::SetSessionConfigOptionRequest::new(self.session_id.clone(), config_id, value),
927        )
928    }
929
930    /// Close the remote session and release its resources.
931    ///
932    /// The agent cancels the session's ongoing work as if `session/cancel` had
933    /// been sent. When that cancels active foreground work, the agent sends the
934    /// work's final updates and an `idle` state update with stop reason
935    /// `cancelled` before it responds to `session/close`. A pending prompt
936    /// whose user message closing kept from being inserted fails with `-32800`
937    /// (request cancelled); one already inserted still succeeds with its
938    /// message ID. Prompt responses need not precede the close response.
939    /// Before it responds, the agent cancels every pending permission request
940    /// and elicitation in the session with `$/cancel_request`.
941    ///
942    /// Existing clones of this local command handle are not invalidated, but
943    /// the session is no longer active: the agent rejects later commands for it
944    /// with `-32002` (resource not found).
945    ///
946    /// Closing is idempotent. An agent should answer a close of an unknown or
947    /// already-closed session with success, and an older agent may answer
948    /// `-32002` instead, so this helper resolves a `-32002` response as a
949    /// successful, empty [`v2::schema::CloseSessionResponse`](v2::CloseSessionResponse). Other errors are
950    /// returned unchanged.
951    ///
952    /// See <https://agentclientprotocol.com/protocol/v2/session-setup#closing-active-sessions>.
953    pub fn close(&self) -> SentRequest<v2::CloseSessionResponse, V2> {
954        self.connection
955            .send_request_to(Agent, v2::CloseSessionRequest::new(self.session_id.clone()))
956            .recover_error(|error| {
957                if error.code == v2::ErrorCode::ResourceNotFound {
958                    Ok(v2::CloseSessionResponse::new())
959                } else {
960                    Err(error)
961                }
962            })
963    }
964}