1use super::error::AcpClientError;
2use super::event::AcpEvent;
3use crate::notifications::{AuthMethodsUpdatedParams, ContextClearedParams, McpNotification, SubAgentProgressParams};
4use agent_client_protocol::schema::v2::{
5 AuthMethod, CancelSessionNotification, CreateElicitationRequest, InitializeRequest, InitializeResponse,
6 NewSessionRequest, NewSessionResponse, PermissionOptionId, PermissionOptionKind, PromptCapabilities, PromptRequest,
7 PromptResponse, ReplayFrom, ReplayFromStart, RequestPermissionOutcome, RequestPermissionRequest,
8 RequestPermissionResponse, ResumeSessionRequest, ResumeSessionResponse, SelectedPermissionOutcome,
9 SessionCapabilities, UpdateSessionNotification,
10};
11use agent_client_protocol::util::MatchDispatchFrom;
12use agent_client_protocol::{
13 self as acp, Client, ConnectTo, ConnectionTo, Dispatch, HandleDispatchFrom, Handled, V2ConnectionTo,
14};
15use std::future::Future;
16use std::sync::Arc;
17use tokio::sync::{Mutex, mpsc, oneshot};
18use tokio::task::JoinHandle;
19use tracing::info;
20
21#[derive(Clone)]
22pub struct AcpClientHandle {
23 cx: V2ConnectionTo<acp::Agent>,
24 connection: Arc<ClientConnection>,
25}
26
27pub struct AcpClient {
28 pub initialize_response: InitializeResponse,
29 pub event_rx: mpsc::UnboundedReceiver<AcpEvent>,
30 pub handle: AcpClientHandle,
31}
32
33pub async fn connect_acp_client(
35 agent: impl ConnectTo<Client> + 'static,
36 init_request: InitializeRequest,
37) -> Result<AcpClient, AcpClientError> {
38 let (event_tx, event_rx) = mpsc::unbounded_channel();
39 let (init_tx, init_rx) = oneshot::channel();
40 let events = ConnectionEvents(event_tx);
41 let driver = tokio::spawn(run_client_connection(agent, init_request, init_tx, events));
42 let connection = Arc::new(ClientConnection { driver: Mutex::new(Some(driver)) });
43 let (initialize_response, cx) = await_response(init_rx).await?;
44 Ok(AcpClient { initialize_response, event_rx, handle: AcpClientHandle { cx, connection } })
45}
46
47impl AcpClient {
48 pub fn agent_name(&self) -> String {
50 let info = &self.initialize_response.info;
51 info.title.as_deref().unwrap_or(&info.name).to_string()
52 }
53
54 pub fn prompt_capabilities(&self) -> Option<&PromptCapabilities> {
55 self.session_capabilities().and_then(|session| session.prompt.as_ref())
56 }
57
58 pub fn session_capabilities(&self) -> Option<&SessionCapabilities> {
59 self.initialize_response.capabilities.session.as_ref()
60 }
61
62 pub fn auth_methods(&self) -> &[AuthMethod] {
63 &self.initialize_response.auth_methods
64 }
65}
66
67impl AcpClientHandle {
68 pub async fn disconnect(&self) {
70 let mut driver = self.connection.driver.lock().await;
71 if let Some(task) = driver.as_mut() {
72 task.abort();
73 let _ = task.await;
74 }
75 *driver = None;
76 }
77
78 pub fn prompt(
79 &self,
80 request: PromptRequest,
81 ) -> impl Future<Output = Result<PromptResponse, AcpClientError>> + Send + use<> {
82 self.request(request)
83 }
84
85 pub fn resume_session_with_replay(
87 &self,
88 request: ResumeSessionRequest,
89 ) -> impl Future<Output = Result<ResumeSessionResponse, AcpClientError>> + Send + use<> {
90 self.request(request.replay_from(ReplayFrom::Start(ReplayFromStart::new())))
91 }
92
93 pub fn new_session(
94 &self,
95 request: NewSessionRequest,
96 ) -> impl Future<Output = Result<NewSessionResponse, AcpClientError>> + Send + use<> {
97 self.request(request)
98 }
99
100 pub fn request<R: acp::JsonRpcRequest>(
102 &self,
103 request: R,
104 ) -> impl Future<Output = Result<R::Response, AcpClientError>> + Send + use<R> {
105 let sent = self.cx.send_request(request);
106 async move { sent.block_task().await.map_err(AcpClientError::Protocol) }
107 }
108
109 pub fn resume_session(
111 &self,
112 request: ResumeSessionRequest,
113 ) -> impl Future<Output = Result<ResumeSessionResponse, AcpClientError>> + Send + use<> {
114 self.request(request.replay_from(None))
115 }
116
117 pub fn cancel(&self, request: CancelSessionNotification) -> Result<(), AcpClientError> {
118 self.notify(request)
119 }
120
121 pub fn notify(&self, request: impl acp::JsonRpcNotification) -> Result<(), AcpClientError> {
122 self.cx.send_notification(request).map_err(AcpClientError::Protocol)
123 }
124}
125
126struct ClientConnection {
127 driver: Mutex<Option<JoinHandle<()>>>,
128}
129
130impl Drop for ClientConnection {
131 fn drop(&mut self) {
132 if let Some(driver) = self.driver.get_mut() {
133 driver.abort();
134 }
135 }
136}
137
138struct ConnectionEvents(mpsc::UnboundedSender<AcpEvent>);
139
140impl Drop for ConnectionEvents {
141 fn drop(&mut self) {
142 let _ = self.0.send(AcpEvent::ConnectionClosed);
143 }
144}
145
146async fn run_client_connection(
147 agent: impl ConnectTo<Client> + 'static,
148 init_request: InitializeRequest,
149 init_tx: oneshot::Sender<Result<(InitializeResponse, V2ConnectionTo<acp::Agent>), AcpClientError>>,
150 events: ConnectionEvents,
151) {
152 let connection_result = Client
153 .v2()
154 .name("wisp")
155 .with_handler(ClientHandlers(events.0.clone()))
156 .connect_with(agent, async move |cx: V2ConnectionTo<acp::Agent>| {
157 let result = cx.send_request(init_request).block_task().await.map_err(AcpClientError::Protocol);
158 let _ = init_tx.send(result.map(|response| {
159 info!("ACP initialized: protocol={:?}, agent_info={:?}", response.protocol_version, response.info);
160 (response, cx.clone())
161 }));
162 cx.incoming_closed().await;
163 Ok(())
164 })
165 .await;
166 if let Err(error) = connection_result {
167 tracing::warn!("ACP connection exited with error: {error:?}");
168 }
169}
170
171struct ClientHandlers(mpsc::UnboundedSender<AcpEvent>);
172
173impl HandleDispatchFrom<acp::Agent> for ClientHandlers {
174 async fn handle_dispatch_from(
175 &mut self,
176 message: Dispatch,
177 cx: ConnectionTo<acp::Agent>,
178 ) -> Result<Handled<Dispatch>, acp::Error> {
179 let emit = |event| {
180 if let Err(error) = self.0.send(event)
181 && let AcpEvent::ElicitationRequest { responder, .. } = error.0
182 {
183 let _ = responder.respond_with_error(acp::Error::internal_error());
184 }
185 Ok::<_, acp::Error>(())
186 };
187 MatchDispatchFrom::new(message, &cx)
188 .if_request(async |request: RequestPermissionRequest, responder| {
189 let outcome = auto_approve_option(&request).map_or(RequestPermissionOutcome::Cancelled, |option| {
190 RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new(option))
191 });
192 let _ = responder.respond(RequestPermissionResponse::new(outcome));
193 Ok(())
194 })
195 .await
196 .if_request(async |params: CreateElicitationRequest, responder| {
197 emit(AcpEvent::ElicitationRequest { params: Box::new(params), responder })
198 })
199 .await
200 .if_notification(async |params: UpdateSessionNotification| emit(params.into()))
201 .await
202 .if_notification(async |params: ContextClearedParams| emit(AcpEvent::ContextCleared(params)))
203 .await
204 .if_notification(async |params: SubAgentProgressParams| emit(AcpEvent::SubAgentProgress(params)))
205 .await
206 .if_notification(async |params: McpNotification| emit(AcpEvent::McpNotification(params)))
207 .await
208 .if_notification(async |params: AuthMethodsUpdatedParams| emit(AcpEvent::AuthMethodsUpdated(params)))
209 .await
210 .done()
211 }
212
213 fn describe_chain(&self) -> impl std::fmt::Debug {
214 "ClientHandlers"
215 }
216}
217
218async fn await_response<T>(receiver: oneshot::Receiver<Result<T, AcpClientError>>) -> Result<T, AcpClientError> {
219 receiver.await.map_err(|_| AcpClientError::AgentCrashed("ACP task ended before responding".to_string()))?
220}
221
222fn auto_approve_option(req: &RequestPermissionRequest) -> Option<PermissionOptionId> {
223 req.options
224 .iter()
225 .find(|option| matches!(option.kind, PermissionOptionKind::AllowOnce | PermissionOptionKind::AllowAlways))
226 .or_else(|| req.options.first())
227 .map(|option| option.option_id.clone())
228}