use std::{future::Future, marker::PhantomData, path::Path};
use futures::channel::{mpsc, oneshot};
use crate::{
Agent, Client, ConnectionTo, Dispatch, HandleDispatchFrom, Handled, JsonRpcRequest, Responder,
Role,
jsonrpc::{
DynamicHandlerGuard,
run::{NullRun, RunWithConnectionTo},
},
role::{HasPeer, acp::ProxySessionMessages},
schema::v1::{
ContentBlock, ContentChunk, LoadSessionRequest, LoadSessionResponse, Meta,
NewSessionRequest, NewSessionResponse, PromptRequest, PromptResponse, ResumeSessionRequest,
ResumeSessionResponse, SessionConfigOption, SessionId, SessionModeState,
SessionNotification, SessionUpdate, StopReason,
},
util::{MatchDispatch, MatchDispatchFrom, run_until},
};
#[cfg(feature = "unstable_mcp_over_acp")]
use crate::{jsonrpc::run::ChainRun, mcp_server::McpServer};
#[cfg(feature = "unstable_protocol_v2")]
mod v2;
#[cfg(feature = "unstable_protocol_v2")]
pub use v2::*;
#[derive(Debug)]
pub struct Blocking;
impl SessionBlockState for Blocking {}
#[derive(Debug)]
pub struct NonBlocking;
impl SessionBlockState for NonBlocking {}
pub trait SessionBlockState: Send + 'static + Sync + std::fmt::Debug {}
impl<Counterpart: Role> ConnectionTo<Counterpart>
where
Counterpart: HasPeer<Agent>,
{
pub fn build_session(&self, cwd: impl AsRef<Path>) -> SessionBuilder<Counterpart, NullRun> {
SessionBuilder::new(self, NewSessionRequest::new(cwd.as_ref()))
}
pub fn build_session_cwd(&self) -> Result<SessionBuilder<Counterpart, NullRun>, crate::Error> {
let cwd = std::env::current_dir().map_err(|e| {
crate::Error::internal_error().data(format!("cannot get current directory: {e}"))
})?;
Ok(self.build_session(cwd))
}
pub fn build_session_from(
&self,
request: NewSessionRequest,
) -> SessionBuilder<Counterpart, NullRun> {
SessionBuilder::new(self, request)
}
pub fn load_session(
&self,
session_id: impl Into<SessionId>,
cwd: impl AsRef<Path>,
) -> RestoreSessionBuilder<Counterpart, LoadSessionRequest> {
self.load_session_from(LoadSessionRequest::new(session_id, cwd.as_ref()))
}
pub fn load_session_from(
&self,
request: LoadSessionRequest,
) -> RestoreSessionBuilder<Counterpart, LoadSessionRequest> {
RestoreSessionBuilder::new(self, request)
}
pub fn resume_session(
&self,
session_id: impl Into<SessionId>,
cwd: impl AsRef<Path>,
) -> RestoreSessionBuilder<Counterpart, ResumeSessionRequest> {
self.resume_session_from(ResumeSessionRequest::new(session_id, cwd.as_ref()))
}
pub fn resume_session_from(
&self,
request: ResumeSessionRequest,
) -> RestoreSessionBuilder<Counterpart, ResumeSessionRequest> {
RestoreSessionBuilder::new(self, request)
}
pub(crate) fn attach_session<'runner>(
&self,
response: NewSessionResponse,
mcp_handler_registrations: Vec<DynamicHandlerGuard<Counterpart>>,
) -> Result<ActiveSession<'runner, Counterpart>, crate::Error> {
let NewSessionResponse {
session_id,
modes,
config_options,
meta,
..
} = response;
let prepared = self.prepare_session_routing(&session_id)?;
Ok(prepared.into_active_session(
self.clone(),
session_id,
modes,
config_options,
meta,
mcp_handler_registrations,
))
}
fn prepare_session_routing(
&self,
session_id: &SessionId,
) -> Result<PreparedSession<Counterpart>, crate::Error> {
let (update_tx, update_rx) = mpsc::unbounded();
let handler = ActiveSessionHandler::new(session_id.clone(), update_tx.clone());
let session_handler_registration = self.add_dynamic_handler(handler)?;
Ok(PreparedSession {
update_rx,
update_tx,
session_handler_registration,
})
}
}
struct PreparedSession<Counterpart: Role>
where
Counterpart: HasPeer<Agent>,
{
update_rx: mpsc::UnboundedReceiver<SessionMessage>,
update_tx: mpsc::UnboundedSender<SessionMessage>,
session_handler_registration: DynamicHandlerGuard<Counterpart>,
}
impl<Counterpart> PreparedSession<Counterpart>
where
Counterpart: HasPeer<Agent>,
{
fn into_active_session<'runner>(
self,
connection: ConnectionTo<Counterpart>,
session_id: SessionId,
modes: Option<SessionModeState>,
config_options: Option<Vec<SessionConfigOption>>,
meta: Option<Meta>,
mcp_handler_registrations: Vec<DynamicHandlerGuard<Counterpart>>,
) -> ActiveSession<'runner, Counterpart> {
ActiveSession {
session_id,
modes,
config_options,
meta,
update_rx: self.update_rx,
update_tx: self.update_tx,
connection,
session_handler_registration: self.session_handler_registration,
mcp_handler_registrations,
_runner: PhantomData,
}
}
}
trait RestoreRequest: JsonRpcRequest {
fn session_id(&self) -> &SessionId;
fn response_modes(response: &Self::Response) -> Option<SessionModeState>;
fn response_config_options(response: &Self::Response) -> Option<Vec<SessionConfigOption>>;
fn response_meta(response: &Self::Response) -> Option<Meta>;
}
impl RestoreRequest for LoadSessionRequest {
fn session_id(&self) -> &SessionId {
&self.session_id
}
fn response_modes(response: &Self::Response) -> Option<SessionModeState> {
response.modes.clone()
}
fn response_config_options(response: &Self::Response) -> Option<Vec<SessionConfigOption>> {
response.config_options.clone()
}
fn response_meta(response: &Self::Response) -> Option<Meta> {
response.meta.clone()
}
}
impl RestoreRequest for ResumeSessionRequest {
fn session_id(&self) -> &SessionId {
&self.session_id
}
fn response_modes(response: &Self::Response) -> Option<SessionModeState> {
response.modes.clone()
}
fn response_config_options(response: &Self::Response) -> Option<Vec<SessionConfigOption>> {
response.config_options.clone()
}
fn response_meta(response: &Self::Response) -> Option<Meta> {
response.meta.clone()
}
}
#[must_use = "use `start_session` or `on_session_start` to restore the session"]
#[derive(Debug)]
pub struct RestoreSessionBuilder<Counterpart, Request, BlockState = NonBlocking>
where
Counterpart: HasPeer<Agent>,
BlockState: SessionBlockState,
{
connection: ConnectionTo<Counterpart>,
request: Request,
block_state: PhantomData<BlockState>,
}
impl<Counterpart, Request> RestoreSessionBuilder<Counterpart, Request, NonBlocking>
where
Counterpart: HasPeer<Agent>,
{
fn new(connection: &ConnectionTo<Counterpart>, request: Request) -> Self {
Self {
connection: connection.clone(),
request,
block_state: PhantomData,
}
}
pub fn block_task(self) -> RestoreSessionBuilder<Counterpart, Request, Blocking> {
RestoreSessionBuilder {
connection: self.connection,
request: self.request,
block_state: PhantomData,
}
}
}
fn restored_session<Counterpart, Request>(
connection: ConnectionTo<Counterpart>,
session_id: SessionId,
prepared: PreparedSession<Counterpart>,
response: Request::Response,
) -> RestoredSession<'static, Counterpart, Request::Response>
where
Counterpart: HasPeer<Agent>,
Request: RestoreRequest,
{
let session = prepared.into_active_session(
connection,
session_id,
Request::response_modes(&response),
Request::response_config_options(&response),
Request::response_meta(&response),
Vec::new(),
);
RestoredSession { session, response }
}
fn on_restore_session_start<Counterpart, Request, F, Fut>(
builder: RestoreSessionBuilder<Counterpart, Request>,
op: F,
) -> Result<(), crate::Error>
where
Counterpart: HasPeer<Agent>,
Request: RestoreRequest,
F: FnOnce(RestoredSession<'static, Counterpart, Request::Response>) -> Fut + Send + 'static,
Fut: Future<Output = Result<(), crate::Error>> + Send,
{
ensure_v1_session_protocol(&builder.connection)?;
let RestoreSessionBuilder {
connection,
request,
block_state: _,
} = builder;
let session_id = request.session_id().clone();
let prepared = connection.prepare_session_routing(&session_id)?;
let routing_ready = connection.dynamic_handler_barrier();
connection
.send_ordered_request_to_after(Agent, request, routing_ready)
.on_receiving_result({
let connection = connection.clone();
async move |result| {
let response = result?;
let restored = restored_session::<_, Request>(
connection.clone(),
session_id,
prepared,
response,
);
connection.spawn(async move { op(restored).await })
}
})
}
async fn start_restored_session<Counterpart, Request>(
builder: RestoreSessionBuilder<Counterpart, Request, Blocking>,
) -> Result<RestoredSession<'static, Counterpart, Request::Response>, crate::Error>
where
Counterpart: HasPeer<Agent>,
Request: RestoreRequest,
{
ensure_v1_session_protocol(&builder.connection)?;
let RestoreSessionBuilder {
connection,
request,
block_state: _,
} = builder;
let session_id = request.session_id().clone();
let prepared = connection.prepare_session_routing(&session_id)?;
let routing_ready = connection.dynamic_handler_barrier();
let session_connection = connection.clone();
connection
.send_ordered_request_to_after(Agent, request, routing_ready)
.block_task_with_ordered_result(move |result| {
let response = result?;
Ok(restored_session::<_, Request>(
session_connection,
session_id,
prepared,
response,
))
})
.await
}
impl<Counterpart> RestoreSessionBuilder<Counterpart, LoadSessionRequest>
where
Counterpart: HasPeer<Agent>,
{
pub fn on_session_start<F, Fut>(self, op: F) -> Result<(), crate::Error>
where
F: FnOnce(RestoredSession<'static, Counterpart, LoadSessionResponse>) -> Fut
+ Send
+ 'static,
Fut: Future<Output = Result<(), crate::Error>> + Send,
{
on_restore_session_start(self, op)
}
}
impl<Counterpart> RestoreSessionBuilder<Counterpart, ResumeSessionRequest>
where
Counterpart: HasPeer<Agent>,
{
pub fn on_session_start<F, Fut>(self, op: F) -> Result<(), crate::Error>
where
F: FnOnce(RestoredSession<'static, Counterpart, ResumeSessionResponse>) -> Fut
+ Send
+ 'static,
Fut: Future<Output = Result<(), crate::Error>> + Send,
{
on_restore_session_start(self, op)
}
}
impl<Counterpart> RestoreSessionBuilder<Counterpart, LoadSessionRequest, Blocking>
where
Counterpart: HasPeer<Agent>,
{
pub async fn start_session(
self,
) -> Result<RestoredSession<'static, Counterpart, LoadSessionResponse>, crate::Error> {
start_restored_session(self).await
}
}
impl<Counterpart> RestoreSessionBuilder<Counterpart, ResumeSessionRequest, Blocking>
where
Counterpart: HasPeer<Agent>,
{
pub async fn start_session(
self,
) -> Result<RestoredSession<'static, Counterpart, ResumeSessionResponse>, crate::Error> {
start_restored_session(self).await
}
}
pub struct RestoredSession<'runner, Link, Response>
where
Link: HasPeer<Agent>,
{
session: ActiveSession<'runner, Link>,
response: Response,
}
impl<'runner, Link, Response> RestoredSession<'runner, Link, Response>
where
Link: HasPeer<Agent>,
{
pub fn session(&self) -> &ActiveSession<'runner, Link> {
&self.session
}
pub fn session_mut(&mut self) -> &mut ActiveSession<'runner, Link> {
&mut self.session
}
pub fn response(&self) -> &Response {
&self.response
}
pub fn into_parts(self) -> (ActiveSession<'runner, Link>, Response) {
(self.session, self.response)
}
pub fn into_session(self) -> ActiveSession<'runner, Link> {
self.session
}
}
impl<Link, Response> std::fmt::Debug for RestoredSession<'_, Link, Response>
where
Link: HasPeer<Agent>,
Response: std::fmt::Debug,
{
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("RestoredSession")
.field("session_id", self.session.session_id())
.field("response", &self.response)
.finish()
}
}
#[must_use = "use `start_session`, `run_until`, or `on_session_start` to start the session"]
#[derive(Debug)]
pub struct SessionBuilder<
Counterpart,
Run: RunWithConnectionTo<Counterpart> = NullRun,
BlockState: SessionBlockState = NonBlocking,
> where
Counterpart: HasPeer<Agent>,
{
connection: ConnectionTo<Counterpart>,
request: NewSessionRequest,
dynamic_handler_registrations: Vec<DynamicHandlerGuard<Counterpart>>,
run: Run,
block_state: PhantomData<BlockState>,
}
impl<Counterpart> SessionBuilder<Counterpart, NullRun, NonBlocking>
where
Counterpart: HasPeer<Agent>,
{
fn new(connection: &ConnectionTo<Counterpart>, request: NewSessionRequest) -> Self {
SessionBuilder {
connection: connection.clone(),
request,
dynamic_handler_registrations: Vec::default(),
run: NullRun,
block_state: PhantomData,
}
}
}
impl<Counterpart, R, BlockState> SessionBuilder<Counterpart, R, BlockState>
where
Counterpart: HasPeer<Agent>,
R: RunWithConnectionTo<Counterpart>,
BlockState: SessionBlockState,
{
#[cfg(feature = "unstable_mcp_over_acp")]
pub fn with_mcp_server<McpRun>(
mut self,
mcp_server: McpServer<Counterpart, McpRun>,
) -> Result<SessionBuilder<Counterpart, ChainRun<R, McpRun>, BlockState>, crate::Error>
where
McpRun: RunWithConnectionTo<Counterpart>,
{
let (handler, mcp_run) = mcp_server.into_handler_and_runner();
self.dynamic_handler_registrations
.push(handler.into_dynamic_handler(&mut self.request, &self.connection)?);
Ok(SessionBuilder {
connection: self.connection,
request: self.request,
dynamic_handler_registrations: self.dynamic_handler_registrations,
run: ChainRun::new(self.run, mcp_run),
block_state: self.block_state,
})
}
pub fn on_session_start<F, Fut>(self, op: F) -> Result<(), crate::Error>
where
R: 'static,
F: FnOnce(ActiveSession<'static, Counterpart>) -> Fut + Send + 'static,
Fut: Future<Output = Result<(), crate::Error>> + Send,
{
ensure_v1_session_protocol(&self.connection)?;
let Self {
connection,
request,
dynamic_handler_registrations,
run,
block_state: _,
} = self;
connection
.send_ordered_request_to(Agent, request)
.on_receiving_result({
let connection = connection.clone();
async move |result| {
let response = result?;
connection.spawn(run.run_with_connection_to(connection.clone()))?;
let active_session =
connection.attach_session(response, dynamic_handler_registrations)?;
connection.spawn(async move { op(active_session).await })
}
})
}
pub fn on_proxy_session_start<F, Fut>(
self,
responder: Responder<NewSessionResponse>,
op: F,
) -> Result<(), crate::Error>
where
F: FnOnce(SessionId) -> Fut + Send + 'static,
Fut: Future<Output = Result<(), crate::Error>> + Send,
Counterpart: HasPeer<Client>,
R: 'static,
{
ensure_v1_session_protocol(&self.connection)?;
let Self {
connection,
request,
dynamic_handler_registrations,
run,
block_state: _,
} = self;
let sent = connection.send_ordered_request_to(Agent, request);
let sent = sent.forward_cancellation_from(responder.cancellation());
sent.on_receiving_ok_result(responder, {
let connection = connection.clone();
async move |response, responder| {
let session_id = response.session_id.clone();
responder.respond(response)?;
connection
.add_dynamic_handler(ProxySessionMessages::new(session_id.clone()))?
.detach();
connection.spawn(run.run_with_connection_to(connection.clone()))?;
dynamic_handler_registrations
.into_iter()
.for_each(DynamicHandlerGuard::detach);
connection.spawn(async move { op(session_id).await })
}
})
}
}
impl<Counterpart, R> SessionBuilder<Counterpart, R, NonBlocking>
where
Counterpart: HasPeer<Agent>,
R: RunWithConnectionTo<Counterpart>,
{
pub fn block_task(self) -> SessionBuilder<Counterpart, R, Blocking> {
SessionBuilder {
connection: self.connection,
request: self.request,
dynamic_handler_registrations: self.dynamic_handler_registrations,
run: self.run,
block_state: PhantomData,
}
}
}
impl<Counterpart, R> SessionBuilder<Counterpart, R, Blocking>
where
Counterpart: HasPeer<Agent>,
R: RunWithConnectionTo<Counterpart>,
{
pub async fn run_until<T>(
self,
op: impl for<'runner> AsyncFnOnce(
ActiveSession<'runner, Counterpart>,
) -> Result<T, crate::Error>,
) -> Result<T, crate::Error> {
ensure_v1_session_protocol(&self.connection)?;
let Self {
connection,
request,
dynamic_handler_registrations,
run,
block_state: _,
} = self;
let response = connection
.send_request_to(Agent, request)
.block_task()
.await?;
let active_session = connection.attach_session(response, dynamic_handler_registrations)?;
run_until(
run.run_with_connection_to(connection.clone()),
op(active_session),
)
.await
}
pub async fn start_session(self) -> Result<ActiveSession<'static, Counterpart>, crate::Error>
where
R: 'static,
{
ensure_v1_session_protocol(&self.connection)?;
let Self {
connection,
request,
dynamic_handler_registrations,
run,
block_state: _,
} = self;
let (active_session_tx, active_session_rx) = oneshot::channel();
connection.clone().spawn(async move {
let response = connection
.send_request_to(Agent, request)
.block_task()
.await?;
connection.spawn(run.run_with_connection_to(connection.clone()))?;
let active_session =
connection.attach_session(response, dynamic_handler_registrations)?;
active_session_tx
.send(active_session)
.map_err(|_| crate::Error::internal_error())?;
Ok(())
})?;
active_session_rx
.await
.map_err(|_| crate::Error::internal_error())
}
pub async fn start_session_proxy(
self,
responder: Responder<NewSessionResponse>,
) -> Result<SessionId, crate::Error>
where
Counterpart: HasPeer<Client>,
R: 'static,
{
let active_session = self.start_session().await?;
let session_id = active_session.session_id().clone();
responder.respond(active_session.response())?;
active_session.proxy_remaining_messages()?;
Ok(session_id)
}
}
#[derive(Debug)]
pub struct ActiveSession<'runner, Link>
where
Link: HasPeer<Agent>,
{
session_id: SessionId,
update_rx: mpsc::UnboundedReceiver<SessionMessage>,
update_tx: mpsc::UnboundedSender<SessionMessage>,
modes: Option<SessionModeState>,
config_options: Option<Vec<SessionConfigOption>>,
meta: Option<serde_json::Map<String, serde_json::Value>>,
connection: ConnectionTo<Link>,
session_handler_registration: DynamicHandlerGuard<Link>,
mcp_handler_registrations: Vec<DynamicHandlerGuard<Link>>,
_runner: PhantomData<&'runner ()>,
}
#[non_exhaustive]
#[derive(Debug)]
#[allow(
clippy::large_enum_variant,
reason = "Dispatch messages vastly outnumber StopReason; boxing would add a heap allocation"
)]
pub enum SessionMessage {
SessionMessage(Dispatch),
StopReason(StopReason),
}
impl<Link> ActiveSession<'_, Link>
where
Link: HasPeer<Agent>,
{
pub fn session_id(&self) -> &SessionId {
&self.session_id
}
pub fn modes(&self) -> Option<&SessionModeState> {
self.modes.as_ref()
}
pub fn config_options(&self) -> Option<&[SessionConfigOption]> {
self.config_options.as_deref()
}
pub fn meta(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
self.meta.as_ref()
}
pub fn response(&self) -> NewSessionResponse {
NewSessionResponse::new(self.session_id.clone())
.modes(self.modes.clone())
.config_options(self.config_options.clone())
.meta(self.meta.clone())
}
pub fn connection(&self) -> &ConnectionTo<Link> {
&self.connection
}
pub fn send_prompt(&mut self, prompt: impl ToString) -> Result<(), crate::Error> {
let update_tx = self.update_tx.clone();
self.connection
.send_ordered_request_to(
Agent,
PromptRequest::new(self.session_id.clone(), vec![prompt.to_string().into()]),
)
.on_receiving_result(async move |result| {
let PromptResponse { stop_reason, .. } = result?;
update_tx
.unbounded_send(SessionMessage::StopReason(stop_reason))
.map_err(crate::util::internal_error)?;
Ok(())
})
}
pub async fn read_update(&mut self) -> Result<SessionMessage, crate::Error> {
use futures::StreamExt;
let message =
self.update_rx.next().await.ok_or_else(|| {
crate::util::internal_error("session channel closed unexpectedly")
})?;
Ok(message)
}
pub async fn read_to_string(&mut self) -> Result<String, crate::Error> {
let mut output = String::new();
loop {
let update = self.read_update().await?;
tracing::trace!(?update, "read_to_string update");
match update {
SessionMessage::SessionMessage(dispatch) => MatchDispatch::new(dispatch)
.if_notification(async |notif: SessionNotification| match notif.update {
SessionUpdate::AgentMessageChunk(ContentChunk {
content: ContentBlock::Text(text),
..
}) => {
output.push_str(&text.text);
Ok(())
}
_ => Ok(()),
})
.await
.otherwise_ignore()?,
SessionMessage::StopReason(_stop_reason) => break,
}
}
Ok(output)
}
}
impl<Link> ActiveSession<'static, Link>
where
Link: HasPeer<Agent>,
{
pub fn proxy_remaining_messages(self) -> Result<(), crate::Error>
where
Link: HasPeer<Client>,
{
let ActiveSession {
session_id,
mut update_rx,
update_tx,
connection,
session_handler_registration,
mcp_handler_registrations,
modes: _,
config_options: _,
meta: _,
_runner,
} = self;
drop(session_handler_registration);
drop(update_tx);
while let Ok(message) = update_rx.try_recv() {
match message {
SessionMessage::SessionMessage(dispatch) => {
connection.send_proxied_message_to(Client, dispatch)?;
}
SessionMessage::StopReason(_) => {
}
}
}
connection
.add_dynamic_handler(ProxySessionMessages::new(session_id))?
.detach();
for registration in mcp_handler_registrations {
registration.detach();
}
Ok(())
}
}
struct ActiveSessionHandler {
session_id: SessionId,
update_tx: mpsc::UnboundedSender<SessionMessage>,
}
impl ActiveSessionHandler {
pub fn new(session_id: SessionId, update_tx: mpsc::UnboundedSender<SessionMessage>) -> Self {
Self {
session_id,
update_tx,
}
}
}
impl<Counterpart: Role> HandleDispatchFrom<Counterpart> for ActiveSessionHandler
where
Counterpart: HasPeer<Agent>,
{
async fn handle_dispatch_from(
&mut self,
message: Dispatch,
cx: ConnectionTo<Counterpart>,
) -> Result<Handled<Dispatch>, crate::Error> {
tracing::trace!(
?message,
handler_session_id = ?self.session_id,
"ActiveSessionHandler::handle_dispatch"
);
MatchDispatchFrom::new(message, &cx)
.if_dispatch_from(Agent, async |message| {
if let Some(session_id) = message.get_session_id()? {
tracing::trace!(
message_session_id = ?session_id,
handler_session_id = ?self.session_id,
"ActiveSessionHandler::handle_dispatch"
);
if session_id == self.session_id {
self.update_tx
.unbounded_send(SessionMessage::SessionMessage(message))
.map_err(crate::util::internal_error)?;
return Ok(Handled::Yes);
}
}
Ok(Handled::No {
message,
retry: false,
})
})
.await
.done()
}
fn describe_chain(&self) -> impl std::fmt::Debug {
format!("ActiveSessionHandler({})", self.session_id)
}
}
#[cfg(not(feature = "unstable_protocol_v2"))]
#[allow(
clippy::unnecessary_wraps,
reason = "signature matches the feature-enabled protocol guard"
)]
fn ensure_v1_session_protocol<Counterpart: Role>(
_connection: &ConnectionTo<Counterpart>,
) -> Result<(), crate::Error> {
Ok(())
}
#[cfg(feature = "unstable_protocol_v2")]
fn ensure_v1_session_protocol<Counterpart: Role>(
connection: &ConnectionTo<Counterpart>,
) -> Result<(), crate::Error> {
if connection.acp_protocol_version() != Some(crate::schema::ProtocolVersion::V2) {
return Ok(());
}
Err(crate::Error::invalid_request().data(
"stable session builders use ACP protocol v1 types, but this is a protocol v2 connection; \
use the `V2ConnectionTo` supplied to `Client.v2()` callbacks",
))
}