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