1use super::error::AcpClientError;
2use super::event::AcpEvent;
3use super::prompt_handle::{AcpPromptHandle, PromptCommand};
4use super::tokio_agent::TokioAcpAgent;
5use crate::notifications::{
6 AuthMethodsUpdatedParams, ContextClearedParams, ContextUsageParams, ElicitationParams, McpNotification, McpRequest,
7 SubAgentProgressParams,
8};
9use agent_client_protocol::schema::{
10 AuthMethod, AuthenticateRequest, CancelNotification, ConfigOptionUpdate, ContentBlock, InitializeRequest,
11 InitializeResponse, ListSessionsRequest, LoadSessionRequest, NewSessionRequest, NewSessionResponse,
12 PermissionOptionId, PermissionOptionKind, PromptCapabilities, PromptRequest, RequestPermissionOutcome,
13 RequestPermissionRequest, RequestPermissionResponse, SelectedPermissionOutcome, SessionCapabilities,
14 SessionConfigOption, SessionId, SessionNotification, SessionUpdate, SetSessionConfigOptionRequest, TextContent,
15};
16use agent_client_protocol::{self as acp, Client, ConnectionTo, JsonRpcRequest};
17use tokio::sync::mpsc;
18use tracing::info;
19
20type InitializeResult = Result<(InitializeResponse, NewSessionResponse), AcpClientError>;
21
22pub struct AcpSession {
24 pub session_id: SessionId,
25 pub agent_name: String,
26 pub prompt_capabilities: PromptCapabilities,
27 pub session_capabilities: SessionCapabilities,
28 pub config_options: Vec<SessionConfigOption>,
29 pub auth_methods: Vec<AuthMethod>,
30 pub event_rx: mpsc::UnboundedReceiver<AcpEvent>,
31 pub prompt_handle: AcpPromptHandle,
32}
33
34pub async fn spawn_acp_session(
40 agent: TokioAcpAgent,
41 init_request: InitializeRequest,
42 new_session_request: NewSessionRequest,
43) -> Result<AcpSession, AcpClientError> {
44 let (event_tx, event_rx) = mpsc::unbounded_channel::<AcpEvent>();
45 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel::<PromptCommand>();
46 let (init_tx, mut init_rx) = mpsc::unbounded_channel::<InitializeResult>();
47 tokio::spawn(run_client_connection(agent, event_tx, cmd_rx, init_tx, init_request, new_session_request));
48
49 let (init_resp, session_resp) = init_rx
50 .recv()
51 .await
52 .ok_or_else(|| AcpClientError::AgentCrashed("ACP task died during initialization".to_string()))??;
53
54 let agent_name = init_resp
55 .agent_info
56 .as_ref()
57 .map_or_else(|| "agent".to_string(), |info| info.title.as_deref().unwrap_or(&info.name).to_string());
58
59 Ok(AcpSession {
60 session_id: session_resp.session_id,
61 agent_name,
62 prompt_capabilities: init_resp.agent_capabilities.prompt_capabilities,
63 session_capabilities: init_resp.agent_capabilities.session_capabilities,
64 config_options: session_resp.config_options.unwrap_or_default(),
65 auth_methods: init_resp.auth_methods,
66 event_rx,
67 prompt_handle: AcpPromptHandle { cmd_tx },
68 })
69}
70
71#[allow(clippy::too_many_lines)]
72async fn run_client_connection(
73 agent: TokioAcpAgent,
74 event_tx: mpsc::UnboundedSender<AcpEvent>,
75 cmd_rx: mpsc::UnboundedReceiver<PromptCommand>,
76 init_tx: mpsc::UnboundedSender<InitializeResult>,
77 init_request: InitializeRequest,
78 new_session_request: NewSessionRequest,
79) {
80 let connection_result = Client
81 .builder()
82 .on_receive_request(
83 async move |req: RequestPermissionRequest, responder, _cx| {
84 responder.respond(RequestPermissionResponse::new(RequestPermissionOutcome::Selected(
85 SelectedPermissionOutcome::new(auto_approve_option(&req)),
86 )))
87 },
88 acp::on_receive_request!(),
89 )
90 .on_receive_request(
91 {
92 let event_tx = event_tx.clone();
93 async move |params: ElicitationParams, responder, _cx| {
94 if let Err(send_err) = event_tx.send(AcpEvent::ElicitationRequest { params, responder }) {
95 if let AcpEvent::ElicitationRequest { responder, .. } = send_err.0 {
97 return responder.respond_with_error(acp::Error::internal_error());
98 }
99 }
100 Ok(())
101 }
102 },
103 acp::on_receive_request!(),
104 )
105 .on_receive_notification(
106 {
107 let event_tx = event_tx.clone();
108 async move |SessionNotification { session_id, update, .. }: SessionNotification, _cx| {
109 let _ = event_tx.send(AcpEvent::SessionUpdate { session_id, update: Box::new(update) });
110 Ok(())
111 }
112 },
113 acp::on_receive_notification!(),
114 )
115 .on_receive_notification(
116 {
117 let event_tx = event_tx.clone();
118 async move |p: ContextUsageParams, _cx| {
119 let _ = event_tx.send(AcpEvent::ContextUsage(p));
120 Ok(())
121 }
122 },
123 acp::on_receive_notification!(),
124 )
125 .on_receive_notification(
126 {
127 let event_tx = event_tx.clone();
128 async move |p: ContextClearedParams, _cx| {
129 let _ = event_tx.send(AcpEvent::ContextCleared(p));
130 Ok(())
131 }
132 },
133 acp::on_receive_notification!(),
134 )
135 .on_receive_notification(
136 {
137 let event_tx = event_tx.clone();
138 async move |p: SubAgentProgressParams, _cx| {
139 let _ = event_tx.send(AcpEvent::SubAgentProgress(p));
140 Ok(())
141 }
142 },
143 acp::on_receive_notification!(),
144 )
145 .on_receive_notification(
146 {
147 let event_tx = event_tx.clone();
148 async move |p: AuthMethodsUpdatedParams, _cx| {
149 let _ = event_tx.send(AcpEvent::AuthMethodsUpdated(p));
150 Ok(())
151 }
152 },
153 acp::on_receive_notification!(),
154 )
155 .on_receive_notification(
156 {
157 let event_tx = event_tx.clone();
158 async move |n: McpNotification, _cx| {
159 let _ = event_tx.send(AcpEvent::McpNotification(n));
160 Ok(())
161 }
162 },
163 acp::on_receive_notification!(),
164 )
165 .connect_with(agent, {
166 let event_tx = event_tx.clone();
167 let init_tx = init_tx.clone();
168 async move |cx: ConnectionTo<acp::Agent>| {
169 run_main(cx, event_tx, cmd_rx, init_tx, init_request, new_session_request).await;
170 Ok(())
171 }
172 })
173 .await;
174
175 if let Err(e) = connection_result {
176 tracing::warn!("ACP connection exited with error: {e:?}");
177 let _ = init_tx.send(Err(AcpClientError::ConnectFailed(e)));
178 }
179 let _ = event_tx.send(AcpEvent::ConnectionClosed);
180}
181
182#[allow(clippy::too_many_lines)]
183async fn run_main(
184 cx: ConnectionTo<acp::Agent>,
185 event_tx: mpsc::UnboundedSender<AcpEvent>,
186 mut cmd_rx: mpsc::UnboundedReceiver<PromptCommand>,
187 init_tx: mpsc::UnboundedSender<InitializeResult>,
188 init_request: InitializeRequest,
189 new_session_request: NewSessionRequest,
190) {
191 let init_resp = match cx.send_request(init_request).block_task().await {
192 Ok(r) => r,
193 Err(e) => {
194 let _ = init_tx.send(Err(AcpClientError::Protocol(e)));
195 return;
196 }
197 };
198 info!("ACP initialized: protocol={:?}, agent_info={:?}", init_resp.protocol_version, init_resp.agent_info);
199
200 let session_resp = match cx.send_request(new_session_request).block_task().await {
201 Ok(r) => r,
202 Err(e) => {
203 let _ = init_tx.send(Err(AcpClientError::Protocol(e)));
204 return;
205 }
206 };
207 info!("ACP session created: {}", session_resp.session_id);
208
209 let _ = init_tx.send(Ok((init_resp, session_resp)));
210
211 while let Some(cmd) = cmd_rx.recv().await {
212 match cmd {
213 PromptCommand::Prompt { session_id, text, content } => {
214 let mut prompt = vec![ContentBlock::Text(TextContent::new(text))];
215 if let Some(extra_content) = content {
216 prompt.extend(extra_content);
217 }
218 let prompt_fut = cx.send_request(PromptRequest::new(session_id, prompt)).block_task();
219 tokio::pin!(prompt_fut);
220
221 loop {
222 tokio::select! {
223 result = &mut prompt_fut => {
224 let event = match result {
225 Ok(resp) => AcpEvent::PromptDone(resp.stop_reason),
226 Err(e) => AcpEvent::PromptError(e),
227 };
228 let _ = event_tx.send(event);
229 break;
230 }
231 Some(cmd) = cmd_rx.recv() => {
232 handle_side_command(&cx, &event_tx, cmd).await;
233 }
234 }
235 }
236 }
237 PromptCommand::ListSessions => {
238 request_to_event(
239 &cx,
240 &event_tx,
241 ListSessionsRequest::new(),
242 |resp| AcpEvent::SessionsListed { sessions: resp.sessions },
243 AcpEvent::PromptError,
244 )
245 .await;
246 }
247 PromptCommand::LoadSession { session_id, cwd } => {
248 request_to_event(
249 &cx,
250 &event_tx,
251 LoadSessionRequest::new(session_id.clone(), cwd),
252 |resp| AcpEvent::SessionLoaded {
253 session_id,
254 config_options: resp.config_options.unwrap_or_default(),
255 },
256 AcpEvent::PromptError,
257 )
258 .await;
259 }
260 PromptCommand::NewSession { cwd } => {
261 request_to_event(
262 &cx,
263 &event_tx,
264 NewSessionRequest::new(cwd),
265 |resp| AcpEvent::NewSessionCreated {
266 session_id: resp.session_id,
267 config_options: resp.config_options.unwrap_or_default(),
268 },
269 AcpEvent::PromptError,
270 )
271 .await;
272 }
273 PromptCommand::MoveWorkspace(params) => {
274 request_to_event(&cx, &event_tx, params, AcpEvent::WorkspaceMoved, |e| AcpEvent::WorkspaceMoveFailed {
275 error: format!("{e}"),
276 })
277 .await;
278 }
279 cmd => handle_side_command(&cx, &event_tx, cmd).await,
280 }
281 }
282}
283
284async fn handle_side_command(
285 cx: &ConnectionTo<acp::Agent>,
286 event_tx: &mpsc::UnboundedSender<AcpEvent>,
287 cmd: PromptCommand,
288) {
289 match cmd {
290 PromptCommand::Cancel { session_id } => {
291 let _ = cx.send_notification(CancelNotification::new(session_id));
292 }
293 PromptCommand::SetConfigOption { session_id, config_id, value } => {
294 let req = SetSessionConfigOptionRequest::new(session_id.clone(), config_id, value);
295 match cx.send_request(req).block_task().await {
296 Ok(resp) => {
297 let update = ConfigOptionUpdate::new(resp.config_options);
298 let _ = event_tx.send(AcpEvent::SessionUpdate {
299 session_id,
300 update: Box::new(SessionUpdate::ConfigOptionUpdate(update)),
301 });
302 }
303 Err(e) => {
304 tracing::warn!("set_session_config_option failed: {e:?}");
305 }
306 }
307 }
308 PromptCommand::Prompt { .. } => {
309 tracing::warn!("ignoring duplicate Prompt while one is in-flight");
310 }
311 PromptCommand::ListSessions => {
312 tracing::warn!("ignoring ListSessions while prompt is in-flight");
313 }
314 PromptCommand::LoadSession { .. } => {
315 tracing::warn!("ignoring LoadSession while prompt is in-flight");
316 }
317 PromptCommand::NewSession { .. } => {
318 tracing::warn!("ignoring NewSession while prompt is in-flight");
319 }
320 PromptCommand::AuthenticateMcpServer { session_id, server_name } => {
321 let msg = McpRequest::Authenticate { session_id: session_id.0.to_string(), server_name };
322 if let Err(e) = cx.send_notification(msg) {
323 tracing::warn!("authenticate_mcp_server notification failed: {e:?}");
324 }
325 }
326 PromptCommand::Authenticate { method_id } => {
327 match cx.send_request(AuthenticateRequest::new(method_id.clone())).block_task().await {
328 Ok(_) => {
329 let _ = event_tx.send(AcpEvent::AuthenticateComplete { method_id });
330 }
331 Err(e) => {
332 tracing::warn!("authenticate failed: {e:?}");
333 let _ = event_tx.send(AcpEvent::AuthenticateFailed { method_id, error: format!("{e:?}") });
334 }
335 }
336 }
337 PromptCommand::SearchPrompts(params) => {
338 let query = params.query.clone();
339 request_to_event(cx, event_tx, params, AcpEvent::PromptSearchResults, |e| AcpEvent::PromptSearchFailed {
340 query,
341 error: format!("{e}"),
342 })
343 .await;
344 }
345 PromptCommand::SessionPreview(params) => {
346 let session_id = params.session_id.clone();
347 request_to_event(cx, event_tx, params, AcpEvent::SessionPreviewLoaded, |e| {
348 AcpEvent::SessionPreviewFailed { session_id, error: format!("{e}") }
349 })
350 .await;
351 }
352 PromptCommand::ListWorkspaces(params) => {
353 request_to_event(cx, event_tx, params, AcpEvent::WorkspacesListed, |e| AcpEvent::WorkspaceListFailed {
354 error: format!("{e}"),
355 })
356 .await;
357 }
358 PromptCommand::MoveWorkspace(_) => {
359 tracing::warn!("ignoring MoveWorkspace while prompt is in-flight");
360 let _ = event_tx.send(AcpEvent::WorkspaceMoveFailed { error: "a prompt is in flight".to_string() });
361 }
362 }
363}
364
365async fn request_to_event<T: JsonRpcRequest>(
366 cx: &ConnectionTo<acp::Agent>,
367 event_tx: &mpsc::UnboundedSender<AcpEvent>,
368 params: T,
369 ok: impl FnOnce(T::Response) -> AcpEvent,
370 err: impl FnOnce(acp::Error) -> AcpEvent,
371) {
372 let event = match cx.send_request(params).block_task().await {
373 Ok(resp) => ok(resp),
374 Err(e) => err(e),
375 };
376 let _ = event_tx.send(event);
377}
378
379fn auto_approve_option(req: &RequestPermissionRequest) -> PermissionOptionId {
380 debug_assert!(!req.options.is_empty(), "ACP guarantees at least one permission option");
381 req.options
382 .iter()
383 .find(|o| matches!(o.kind, PermissionOptionKind::AllowOnce | PermissionOptionKind::AllowAlways))
384 .map_or_else(|| req.options[0].option_id.clone(), |o| o.option_id.clone())
385}