1use std::path::{Path, PathBuf};
4use std::sync::Arc;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
7use std::thread;
8use std::time::{Duration, Instant};
9
10use agent_client_protocol::schema::ProtocolVersion;
11use agent_client_protocol::schema::v1::*;
12use agent_client_protocol::{AcpAgent, Agent, Client, ConnectionTo, JsonRpcRequest, LineDirection};
13use futures::future::{Either, FutureExt};
14use futures::{channel::oneshot, future::select};
15
16use crate::acp::{config, events, fs, permissions, terminal};
17use crate::app::{AgentEvent, ToolStatus};
18use crate::config::AcpAgentConfig;
19use crate::mcp::config::{McpConfig, McpServerConfig, McpTransport};
20use crate::session::AcpSessionMetadata;
21use thndrs_agent::CancelToken;
22
23const CANCEL_POLL_INTERVAL: Duration = Duration::from_millis(50);
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26enum WatchSignal {
27 Cancelled,
28 TimedOut,
29}
30
31enum TimedRequestError {
32 Protocol(agent_client_protocol::Error),
33 Timeout(String),
34 Cancelled,
35}
36
37impl From<TimedRequestError> for agent_client_protocol::Error {
38 fn from(val: TimedRequestError) -> Self {
39 match val {
40 TimedRequestError::Protocol(error) => error,
41 TimedRequestError::Timeout(message) => agent_client_protocol::util::internal_error(message),
42 TimedRequestError::Cancelled => {
43 agent_client_protocol::util::internal_error("ACP request cancelled".to_string())
44 }
45 }
46 }
47}
48
49#[derive(Clone, Debug)]
51pub struct RunHandle {
52 pub root: PathBuf,
53 pub name: String,
54 pub agent: Option<AcpAgentConfig>,
55 pub mcp_config: Option<McpConfig>,
56 pub mcp_diagnostics: Vec<String>,
57 pub prompt: String,
58 pub cancel: CancelToken,
59}
60
61impl RunHandle {
62 pub fn new(root: PathBuf, name: String, agent: Option<AcpAgentConfig>, prompt: String) -> Self {
64 Self { root, name, agent, mcp_config: None, mcp_diagnostics: Vec::new(), prompt, cancel: CancelToken::new() }
65 }
66
67 pub fn with_mcp_config(mut self, mcp_config: McpConfig) -> Self {
69 self.mcp_config = Some(mcp_config);
70 self
71 }
72
73 pub fn with_mcp_diagnostics(mut self, diagnostics: Vec<String>) -> Self {
75 self.mcp_diagnostics = diagnostics;
76 self
77 }
78
79 pub fn spawn(self) -> Receiver<AgentEvent> {
81 let (tx, rx) = mpsc::channel::<AgentEvent>();
82 thread::spawn(move || run(&self, &tx));
83 rx
84 }
85}
86
87#[derive(Clone, Debug, Eq, PartialEq)]
89pub struct ListedSession {
90 pub session_id: String,
92 pub cwd: PathBuf,
94 pub additional_directories: Vec<PathBuf>,
96 pub title: Option<String>,
98 pub updated_at: Option<String>,
100}
101
102impl From<SessionInfo> for ListedSession {
103 fn from(session: SessionInfo) -> Self {
104 ListedSession {
105 session_id: session.session_id.to_string(),
106 cwd: session.cwd,
107 additional_directories: session.additional_directories,
108 title: session.title,
109 updated_at: session.updated_at,
110 }
111 }
112}
113
114struct SessionRunContext<'a> {
115 name: &'a str,
116 command: &'a str,
117 root: &'a Path,
118 mcp_config: Option<&'a McpConfig>,
119 mcp_diagnostics: &'a [String],
120 prompt: &'a str,
121 cancel: &'a CancelToken,
122 timeout_secs: u64,
123 tx: &'a Sender<AgentEvent>,
124}
125
126pub fn logout(name: &str, agent_config: AcpAgentConfig) -> Result<Vec<String>, String> {
128 if !agent_config.enabled {
129 return Err(format!("ACP agent `{name}` is disabled"));
130 }
131 futures::executor::block_on(logout_async(name.to_string(), agent_config))
132}
133
134pub fn list_sessions(name: &str, agent_config: AcpAgentConfig, root: PathBuf) -> Result<Vec<ListedSession>, String> {
136 validate_admin_agent(name, &agent_config)?;
137 futures::executor::block_on(list_sessions_async(name.to_string(), agent_config, root))
138}
139
140pub fn load_session(
142 name: &str, agent_config: AcpAgentConfig, root: PathBuf, session_id: String,
143) -> Result<Vec<AgentEvent>, String> {
144 validate_admin_agent(name, &agent_config)?;
145 futures::executor::block_on(load_session_async(name.to_string(), agent_config, root, session_id))
146}
147
148pub fn resume_session(
150 name: &str, agent_config: AcpAgentConfig, root: PathBuf, session_id: String,
151) -> Result<AcpSessionMetadata, String> {
152 validate_admin_agent(name, &agent_config)?;
153 futures::executor::block_on(resume_session_async(name.to_string(), agent_config, root, session_id))
154}
155
156pub fn close_session(name: &str, agent_config: AcpAgentConfig, session_id: String) -> Result<Vec<String>, String> {
158 validate_admin_agent(name, &agent_config)?;
159 futures::executor::block_on(close_session_async(name.to_string(), agent_config, session_id))
160}
161
162pub fn new_session_request(
163 root: PathBuf, mcp_config: Option<&McpConfig>, capabilities: &AgentCapabilities,
164) -> (NewSessionRequest, Vec<String>) {
165 let Some(config) = mcp_config else {
166 return (NewSessionRequest::new(root), Vec::new());
167 };
168
169 let mut servers = Vec::new();
170 let mut diagnostics = Vec::new();
171 for (name, server) in &config.servers {
172 if !server.enabled {
173 diagnostics.push(format!("acp: MCP server `{name}` not passed because it is disabled"));
174 continue;
175 }
176 match acp_mcp_server(name, server, capabilities) {
177 Some(server) => servers.push(server),
178 None => diagnostics.push(format!(
179 "acp: MCP server `{name}` not passed because its transport is unsupported by the ACP agent"
180 )),
181 }
182 }
183
184 if !servers.is_empty() {
185 diagnostics.push(format!(
186 "acp: passing {} MCP server{} through session/new",
187 servers.len(),
188 if servers.len() == 1 { "" } else { "s" }
189 ));
190 }
191
192 (NewSessionRequest::new(root).mcp_servers(servers), diagnostics)
193}
194
195async fn logout_async(name: String, agent_config: AcpAgentConfig) -> Result<Vec<String>, String> {
196 let agent = build_agent(&agent_config)?;
197 let timeout = Duration::from_secs(agent_config.timeout_secs.max(1));
198 let cancel = CancelToken::new();
199 let error_name = name.clone();
200
201 Client
202 .builder()
203 .connect_with(agent, async move |connection: ConnectionTo<Agent>| {
204 let initialize = request_with_timeout(
205 &connection,
206 InitializeRequest::new(ProtocolVersion::V1),
207 timeout,
208 &name,
209 "initialize",
210 &cancel,
211 )
212 .await
213 .map_err(timed_request_to_error)?;
214 if initialize.protocol_version != ProtocolVersion::V1 {
215 return Err(agent_client_protocol::util::internal_error(format!(
216 "ACP agent `{name}` selected unsupported protocol version {:?}",
217 initialize.protocol_version
218 )));
219 }
220 if initialize.agent_capabilities.auth.logout.is_none() {
221 return Ok(vec![format!("acp: `{name}` does not advertise logout support")]);
222 }
223 request_with_timeout(&connection, LogoutRequest::new(), timeout, &name, "logout", &cancel)
224 .await
225 .map_err(timed_request_to_error)?;
226 Ok(vec![format!("acp: logged out `{name}`")])
227 })
228 .await
229 .map_err(|err| format!("ACP agent `{error_name}` logout failed: {err}"))
230}
231
232async fn list_sessions_async(
233 name: String, agent_config: AcpAgentConfig, root: PathBuf,
234) -> Result<Vec<ListedSession>, String> {
235 let agent = build_agent(&agent_config)?;
236 let timeout = Duration::from_secs(agent_config.timeout_secs.max(1));
237 let cancel = CancelToken::new();
238 let error_name = name.clone();
239
240 Client
241 .builder()
242 .connect_with(agent, async move |connection: ConnectionTo<Agent>| {
243 let initialize = initialize_for_admin(&connection, &name, timeout, &cancel).await?;
244 if initialize.agent_capabilities.session_capabilities.list.is_none() {
245 return Err(agent_client_protocol::util::internal_error(format!(
246 "ACP agent `{name}` does not advertise session/list support"
247 )));
248 }
249 let mut sessions = Vec::new();
250 let mut cursor = None;
251 loop {
252 let response = request_with_timeout(
253 &connection,
254 ListSessionsRequest::new()
255 .cwd(Some(root.clone()))
256 .cursor(cursor.clone()),
257 timeout,
258 &name,
259 "session list",
260 &cancel,
261 )
262 .await
263 .map_err(timed_request_to_error)?;
264 sessions.extend(response.sessions.into_iter().map(|s| s.into()));
265 let Some(next_cursor) = response.next_cursor else {
266 break;
267 };
268 cursor = Some(next_cursor);
269 }
270 Ok(sessions)
271 })
272 .await
273 .map_err(|err| format!("ACP agent `{error_name}` session list failed: {err}"))
274}
275
276async fn load_session_async(
277 name: String, agent_config: AcpAgentConfig, root: PathBuf, session_id: String,
278) -> Result<Vec<AgentEvent>, String> {
279 let agent = build_agent(&agent_config)?;
280 let timeout = Duration::from_secs(agent_config.timeout_secs.max(1));
281 let cancel = CancelToken::new();
282 let events = Arc::new(std::sync::Mutex::new(Vec::new()));
283 let update_events = events.clone();
284 let error_name = name.clone();
285
286 Client
287 .builder()
288 .on_receive_notification(
289 async move |notification: SessionNotification, _cx| {
290 let mut guard = update_events.lock().expect("ACP session load event lock");
291 guard.extend(events::map_session_update(notification.update));
292 Ok(())
293 },
294 agent_client_protocol::on_receive_notification!(),
295 )
296 .connect_with(agent, async move |connection: ConnectionTo<Agent>| {
297 let initialize = initialize_for_admin(&connection, &name, timeout, &cancel).await?;
298 if !initialize.agent_capabilities.load_session {
299 return Err(agent_client_protocol::util::internal_error(format!(
300 "ACP agent `{name}` does not advertise session/load support"
301 )));
302 }
303 request_with_timeout(
304 &connection,
305 LoadSessionRequest::new(SessionId::new(session_id), root),
306 timeout,
307 &name,
308 "session load",
309 &cancel,
310 )
311 .await
312 .map_err(timed_request_to_error)?;
313 Ok(events.lock().expect("ACP session load event lock").clone())
314 })
315 .await
316 .map_err(|err| format!("ACP agent `{error_name}` session load failed: {err}"))
317}
318
319async fn resume_session_async(
320 name: String, agent_config: AcpAgentConfig, root: PathBuf, session_id: String,
321) -> Result<AcpSessionMetadata, String> {
322 let agent = build_agent(&agent_config)?;
323 let timeout = Duration::from_secs(agent_config.timeout_secs.max(1));
324 let cancel = CancelToken::new();
325 let command = config::redacted_command_display(&agent_config);
326 let error_name = name.clone();
327
328 Client
329 .builder()
330 .connect_with(agent, async move |connection: ConnectionTo<Agent>| {
331 let initialize = initialize_for_admin(&connection, &name, timeout, &cancel).await?;
332 if initialize.agent_capabilities.session_capabilities.resume.is_none() {
333 return Err(agent_client_protocol::util::internal_error(format!(
334 "ACP agent `{name}` does not advertise session/resume support"
335 )));
336 }
337 request_with_timeout(
338 &connection,
339 ResumeSessionRequest::new(SessionId::new(session_id.clone()), root),
340 timeout,
341 &name,
342 "session resume",
343 &cancel,
344 )
345 .await
346 .map_err(timed_request_to_error)?;
347 Ok(AcpSessionMetadata {
348 agent_name: name,
349 acp_session_id: session_id,
350 command,
351 protocol_version: format!("{:?}", initialize.protocol_version),
352 agent_info_name: initialize.agent_info.as_ref().map(|info| info.name.clone()),
353 agent_info_version: initialize.agent_info.as_ref().map(|info| info.version.clone()),
354 client_info_name: None,
355 client_info_version: None,
356 })
357 })
358 .await
359 .map_err(|err| format!("ACP agent `{error_name}` session resume failed: {err}"))
360}
361
362async fn close_session_async(
363 name: String, agent_config: AcpAgentConfig, session_id: String,
364) -> Result<Vec<String>, String> {
365 let agent = build_agent(&agent_config)?;
366 let timeout = Duration::from_secs(agent_config.timeout_secs.max(1));
367 let cancel = CancelToken::new();
368 let error_name = name.clone();
369
370 Client
371 .builder()
372 .connect_with(agent, async move |connection: ConnectionTo<Agent>| {
373 let initialize = initialize_for_admin(&connection, &name, timeout, &cancel).await?;
374 if initialize.agent_capabilities.session_capabilities.close.is_none() {
375 return Err(agent_client_protocol::util::internal_error(format!(
376 "ACP agent `{name}` does not advertise session/close support"
377 )));
378 }
379 request_with_timeout(
380 &connection,
381 CloseSessionRequest::new(SessionId::new(session_id.clone())),
382 timeout,
383 &name,
384 "session close",
385 &cancel,
386 )
387 .await
388 .map_err(timed_request_to_error)?;
389 Ok(vec![format!("acp: closed `{name}` session {session_id}")])
390 })
391 .await
392 .map_err(|err| format!("ACP agent `{error_name}` session close failed: {err}"))
393}
394
395fn run(handle: &RunHandle, tx: &Sender<AgentEvent>) {
396 if send(tx, &handle.cancel, AgentEvent::Started).is_none() {
397 return;
398 }
399
400 let Some(agent_config) = handle.agent.clone() else {
401 let _ = send(
402 tx,
403 &handle.cancel,
404 AgentEvent::Failed(format!("ACP agent `{}` is not configured", handle.name)),
405 );
406 return;
407 };
408
409 if !agent_config.enabled {
410 let _ = send(
411 tx,
412 &handle.cancel,
413 AgentEvent::Failed(format!("ACP agent `{}` is disabled", handle.name)),
414 );
415 return;
416 }
417
418 if send(
419 tx,
420 &handle.cancel,
421 AgentEvent::Status(format!(
422 "acp: starting `{}` with {}",
423 handle.name,
424 config::redacted_command_display(&agent_config)
425 )),
426 )
427 .is_none()
428 {
429 return;
430 }
431
432 let result = futures::executor::block_on(run_async(handle.clone(), agent_config, tx.clone()));
433 if let Err(message) = result {
434 let _ = send(tx, &handle.cancel, AgentEvent::Failed(message));
435 }
436}
437
438async fn run_async(handle: RunHandle, agent_config: AcpAgentConfig, tx: Sender<AgentEvent>) -> Result<(), String> {
439 let agent = build_agent(&agent_config)?.with_debug({
440 let tx = tx.clone();
441 move |line, direction| {
442 if direction == LineDirection::Stderr {
443 let _ = tx.send(AgentEvent::Status(format!("acp stderr: {line}")));
444 }
445 }
446 });
447
448 let update_tx = tx.clone();
449 let permission_tx = tx.clone();
450 let read_tx = tx.clone();
451 let write_tx = tx.clone();
452 let root = handle.root.clone();
453 let read_root = handle.root.clone();
454 let write_root = handle.root.clone();
455 let terminal_root = handle.root.clone();
456 let prompt = handle.prompt.clone();
457 let cancel = handle.cancel.clone();
458 let permission_cancel = handle.cancel.clone();
459 let name = handle.name.clone();
460 let timeout_secs = agent_config.timeout_secs;
461 let terminal_registry = Arc::new(terminal::TerminalRegistry::new());
462
463 Client
464 .builder()
465 .on_receive_notification(
466 async move |notification: SessionNotification, _cx| {
467 for event in events::map_session_update(notification.update) {
468 let _ = update_tx.send(event);
469 }
470 Ok(())
471 },
472 agent_client_protocol::on_receive_notification!(),
473 )
474 .on_receive_request(
475 async move |request: RequestPermissionRequest, responder, _cx| {
476 let (decision_tx, decision_rx) = mpsc::channel();
477 let pending = permissions::PendingPermission::from_request(&request, decision_tx);
478 let request_id = pending.tool_call_id.clone();
479 let _ = permission_tx.send(AgentEvent::PermissionRequest(pending));
480 let outcome = loop {
481 if permission_cancel.is_cancelled() {
482 break RequestPermissionOutcome::Cancelled;
483 }
484 match decision_rx.recv_timeout(CANCEL_POLL_INTERVAL) {
485 Ok(permissions::PermissionDecision::Selected(option_id)) => {
486 break RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new(option_id));
487 }
488 Ok(permissions::PermissionDecision::Cancelled) | Err(RecvTimeoutError::Disconnected) => {
489 break RequestPermissionOutcome::Cancelled;
490 }
491 Err(RecvTimeoutError::Timeout) => {}
492 }
493 };
494 let _ = permission_tx.send(AgentEvent::PermissionResolved {
495 tool_call_id: request_id,
496 outcome: permission_outcome_label(&outcome).to_string(),
497 });
498 responder.respond(RequestPermissionResponse::new(outcome))
499 },
500 agent_client_protocol::on_receive_request!(),
501 )
502 .on_receive_request(
503 async move |request: ReadTextFileRequest, responder, _cx| {
504 let id = format!("acp-read-{}", request.path.display());
505 let args = serde_json::json!({
506 "path": request.path.display().to_string(),
507 "line": request.line,
508 "limit": request.limit,
509 })
510 .to_string();
511 let _ = read_tx.send(AgentEvent::ToolStarted {
512 id: id.clone(),
513 name: "acp.fs.read_text_file".to_string(),
514 arguments: args,
515 });
516 match fs::read_text_file(&request.path, &read_root, request.line, request.limit) {
517 Ok(result) => {
518 let _ = read_tx.send(AgentEvent::ToolFinished {
519 id,
520 output: result.output,
521 status: ToolStatus::Ok,
522 write_result: None,
523 shell_result: None,
524 });
525 responder.respond(ReadTextFileResponse::new(result.content))
526 }
527 Err(message) => {
528 let (output, status) = fs::failed_output(&message);
529 let _ = read_tx.send(AgentEvent::ToolFinished {
530 id,
531 output,
532 status,
533 write_result: None,
534 shell_result: None,
535 });
536 responder.respond_with_error(agent_client_protocol::util::internal_error(message))
537 }
538 }
539 },
540 agent_client_protocol::on_receive_request!(),
541 )
542 .on_receive_request(
543 async move |request: WriteTextFileRequest, responder, _cx| {
544 let id = format!("acp-write-{}", request.path.display());
545 let args = serde_json::json!({
546 "path": request.path.display().to_string(),
547 "content_bytes": request.content.len(),
548 })
549 .to_string();
550 let _ = write_tx.send(AgentEvent::ToolStarted {
551 id: id.clone(),
552 name: "acp.fs.write_text_file".to_string(),
553 arguments: args,
554 });
555 match fs::write_text_file(&request.path, &write_root, &request.content) {
556 Ok(result) => {
557 let _ = write_tx.send(AgentEvent::ToolFinished {
558 id,
559 output: result.output,
560 status: ToolStatus::Ok,
561 write_result: Some(result.write_result),
562 shell_result: None,
563 });
564 responder.respond(WriteTextFileResponse::new())
565 }
566 Err(message) => {
567 let (output, status) = fs::failed_output(&message);
568 let _ = write_tx.send(AgentEvent::ToolFinished {
569 id,
570 output,
571 status,
572 write_result: None,
573 shell_result: None,
574 });
575 responder.respond_with_error(agent_client_protocol::util::internal_error(message))
576 }
577 }
578 },
579 agent_client_protocol::on_receive_request!(),
580 )
581 .on_receive_request(
582 {
583 let terminal_registry = terminal_registry.clone();
584 let terminal_tx = tx.clone();
585 let terminal_root = terminal_root.clone();
586 async move |request: CreateTerminalRequest, responder, _cx| match terminal_registry
587 .create(&request, &terminal_root)
588 {
589 Ok((response, event)) => {
590 let _ = terminal_tx.send(event);
591 responder.respond(response)
592 }
593 Err(message) => responder.respond_with_error(agent_client_protocol::util::internal_error(message)),
594 }
595 },
596 agent_client_protocol::on_receive_request!(),
597 )
598 .on_receive_request(
599 {
600 let terminal_registry = terminal_registry.clone();
601 let terminal_tx = tx.clone();
602 async move |request: TerminalOutputRequest, responder, _cx| match terminal_registry.output(&request) {
603 Ok((response, event)) => {
604 let _ = terminal_tx.send(event);
605 responder.respond(response)
606 }
607 Err(message) => responder.respond_with_error(agent_client_protocol::util::internal_error(message)),
608 }
609 },
610 agent_client_protocol::on_receive_request!(),
611 )
612 .on_receive_request(
613 {
614 let terminal_registry = terminal_registry.clone();
615 let terminal_tx = tx.clone();
616 async move |request: WaitForTerminalExitRequest, responder, _cx| match terminal_registry
617 .wait_for_exit(&request)
618 {
619 Ok((response, event)) => {
620 let _ = terminal_tx.send(event);
621 responder.respond(response)
622 }
623 Err(message) => responder.respond_with_error(agent_client_protocol::util::internal_error(message)),
624 }
625 },
626 agent_client_protocol::on_receive_request!(),
627 )
628 .on_receive_request(
629 {
630 let terminal_registry = terminal_registry.clone();
631 let terminal_tx = tx.clone();
632 async move |request: KillTerminalRequest, responder, _cx| match terminal_registry.kill(&request) {
633 Ok((response, event)) => {
634 let _ = terminal_tx.send(event);
635 responder.respond(response)
636 }
637 Err(message) => responder.respond_with_error(agent_client_protocol::util::internal_error(message)),
638 }
639 },
640 agent_client_protocol::on_receive_request!(),
641 )
642 .on_receive_request(
643 {
644 let terminal_registry = terminal_registry.clone();
645 let terminal_tx = tx.clone();
646 async move |request: ReleaseTerminalRequest, responder, _cx| match terminal_registry.release(&request) {
647 Ok((response, event)) => {
648 let _ = terminal_tx.send(event);
649 responder.respond(response)
650 }
651 Err(message) => responder.respond_with_error(agent_client_protocol::util::internal_error(message)),
652 }
653 },
654 agent_client_protocol::on_receive_request!(),
655 )
656 .connect_with(agent, async move |connection: ConnectionTo<Agent>| {
657 let command = config::redacted_command_display(&agent_config);
658 let context = SessionRunContext {
659 name: &name,
660 command: &command,
661 root: &root,
662 mcp_config: handle.mcp_config.as_ref(),
663 mcp_diagnostics: &handle.mcp_diagnostics,
664 prompt: &prompt,
665 cancel: &cancel,
666 timeout_secs,
667 tx: &tx,
668 };
669 run_session(connection, context).await
670 })
671 .await
672 .map_err(|err| format!("ACP agent `{}` failed: {err}", handle.name))
673}
674
675fn permission_outcome_label(outcome: &RequestPermissionOutcome) -> &'static str {
676 match outcome {
677 RequestPermissionOutcome::Cancelled => "cancelled",
678 RequestPermissionOutcome::Selected(_) => "selected",
679 _ => "unknown",
680 }
681}
682
683async fn authenticate_if_advertised(
684 connection: &ConnectionTo<Agent>, name: &str, timeout: Duration, cancel: &CancelToken, tx: &Sender<AgentEvent>,
685 methods: &[AuthMethod],
686) -> Result<(), agent_client_protocol::Error> {
687 let Some(method) = methods.first() else {
688 return Ok(());
689 };
690 let method_id = method.id().clone();
691 let _ = tx.send(AgentEvent::Status(format!(
692 "acp: authenticating `{name}` with {}",
693 method.name()
694 )));
695 match request_with_timeout(
696 connection,
697 AuthenticateRequest::new(method_id.clone()),
698 timeout,
699 name,
700 "authentication",
701 cancel,
702 )
703 .await
704 {
705 Ok(_) => {
706 let _ = tx.send(AgentEvent::Status(format!(
707 "acp: authentication succeeded for `{name}` via {method_id}"
708 )));
709 Ok(())
710 }
711 Err(TimedRequestError::Cancelled) => {
712 let _ = tx.send(AgentEvent::Cancelled);
713 Ok(())
714 }
715 Err(TimedRequestError::Timeout(message)) => Err(agent_client_protocol::util::internal_error(message)),
716 Err(TimedRequestError::Protocol(error)) => {
717 let _ = tx.send(AgentEvent::Failed(format!(
718 "ACP agent `{name}` authentication failed for method {method_id}"
719 )));
720 Err(error)
721 }
722 }
723}
724
725async fn initialize_for_admin(
726 connection: &ConnectionTo<Agent>, name: &str, timeout: Duration, cancel: &CancelToken,
727) -> Result<agent_client_protocol::schema::v1::InitializeResponse, agent_client_protocol::Error> {
728 let initialize = request_with_timeout(
729 connection,
730 InitializeRequest::new(ProtocolVersion::V1),
731 timeout,
732 name,
733 "initialize",
734 cancel,
735 )
736 .await
737 .map_err(timed_request_to_error)?;
738 if initialize.protocol_version != ProtocolVersion::V1 {
739 return Err(agent_client_protocol::util::internal_error(format!(
740 "ACP agent `{name}` selected unsupported protocol version {:?}",
741 initialize.protocol_version
742 )));
743 }
744 let (tx, _rx) = mpsc::channel();
745 authenticate_if_advertised(connection, name, timeout, cancel, &tx, &initialize.auth_methods).await?;
746 Ok(initialize)
747}
748
749async fn run_session(
750 connection: ConnectionTo<Agent>, context: SessionRunContext<'_>,
751) -> Result<(), agent_client_protocol::Error> {
752 let SessionRunContext { name, command, root, mcp_config, mcp_diagnostics, prompt, cancel, timeout_secs, tx } =
753 context;
754 if cancel.is_cancelled() {
755 let _ = tx.send(AgentEvent::Cancelled);
756 return Ok(());
757 }
758
759 let timeout = Duration::from_secs(timeout_secs.max(1));
760 let initialize_request = InitializeRequest::new(ProtocolVersion::V1).client_capabilities(
761 ClientCapabilities::new()
762 .fs(FileSystemCapabilities::new().read_text_file(true).write_text_file(true))
763 .terminal(true),
764 );
765 let initialize = match request_with_timeout(&connection, initialize_request, timeout, name, "initialize", cancel)
766 .await
767 {
768 Ok(response) => response,
769 Err(TimedRequestError::Cancelled) => {
770 let _ = tx.send(AgentEvent::Cancelled);
771 return Ok(());
772 }
773 Err(TimedRequestError::Timeout(message)) => return Err(agent_client_protocol::util::internal_error(message)),
774 Err(TimedRequestError::Protocol(error)) => return Err(error),
775 };
776 if initialize.protocol_version != ProtocolVersion::V1 {
777 let _ = tx.send(AgentEvent::Failed(format!(
778 "ACP agent `{name}` selected unsupported protocol version {:?}",
779 initialize.protocol_version
780 )));
781 return Ok(());
782 }
783 authenticate_if_advertised(&connection, name, timeout, cancel, tx, &initialize.auth_methods).await?;
784 let protocol_version = format!("{:?}", initialize.protocol_version);
785 let agent_info = initialize.agent_info;
786 if let Some(info) = &agent_info {
787 let _ = tx.send(AgentEvent::Status(format!(
788 "acp: connected to {} {}",
789 info.name, info.version
790 )));
791 } else {
792 let _ = tx.send(AgentEvent::Status(format!("acp: connected to `{name}`")));
793 }
794
795 for diagnostic in mcp_diagnostics {
796 let _ = tx.send(AgentEvent::Status(format!("acp: {diagnostic}")));
797 }
798 let (new_session_request, mcp_diagnostics) =
799 new_session_request(root.to_path_buf(), mcp_config, &initialize.agent_capabilities);
800 for diagnostic in mcp_diagnostics {
801 let _ = tx.send(AgentEvent::Status(diagnostic));
802 }
803
804 let session = match request_with_timeout(
805 &connection,
806 new_session_request,
807 timeout,
808 name,
809 "session creation",
810 cancel,
811 )
812 .await
813 {
814 Ok(response) => response,
815 Err(TimedRequestError::Cancelled) => {
816 let _ = tx.send(AgentEvent::Cancelled);
817 return Ok(());
818 }
819 Err(TimedRequestError::Timeout(message)) => return Err(agent_client_protocol::util::internal_error(message)),
820 Err(TimedRequestError::Protocol(error)) => return Err(error),
821 };
822 let _ = tx.send(AgentEvent::Status(format!("acp: session {}", session.session_id)));
823 let _ = tx.send(AgentEvent::AcpSession(AcpSessionMetadata {
824 agent_name: name.to_string(),
825 acp_session_id: session.session_id.to_string(),
826 command: command.to_string(),
827 protocol_version,
828 agent_info_name: agent_info.as_ref().map(|info| info.name.clone()),
829 agent_info_version: agent_info.as_ref().map(|info| info.version.clone()),
830 client_info_name: None,
831 client_info_version: None,
832 }));
833
834 if cancel.is_cancelled() {
835 send_session_cancel(&connection, &session.session_id, tx);
836 let _ = tx.send(AgentEvent::Cancelled);
837 return Ok(());
838 }
839
840 let session_id = session.session_id.clone();
841 let response = match prompt_with_cancel(
842 &connection,
843 PromptRequest::new(session.session_id, vec![ContentBlock::Text(TextContent::new(prompt))]),
844 session_id,
845 timeout,
846 name,
847 cancel,
848 tx,
849 )
850 .await
851 {
852 Ok(response) => response,
853 Err(TimedRequestError::Cancelled) => {
854 let _ = tx.send(AgentEvent::Cancelled);
855 return Ok(());
856 }
857 Err(TimedRequestError::Timeout(message)) => return Err(agent_client_protocol::util::internal_error(message)),
858 Err(TimedRequestError::Protocol(error)) => return Err(error),
859 };
860 send_stop_reason(tx, response.stop_reason);
861 Ok(())
862}
863
864fn acp_mcp_server(name: &str, server: &McpServerConfig, agent_capabilities: &AgentCapabilities) -> Option<McpServer> {
865 match server.transport {
866 McpTransport::Stdio => Some(McpServer::Stdio(
867 McpServerStdio::new(name.to_string(), PathBuf::from(&server.command))
868 .args(server.args.clone())
869 .env(
870 server
871 .env
872 .iter()
873 .map(|(name, value)| EnvVariable::new(name.clone(), value.clone()))
874 .collect(),
875 ),
876 )),
877 McpTransport::StreamableHttp if agent_capabilities.mcp_capabilities.http => {
878 let url = server.url.as_ref()?;
879 Some(McpServer::Http(
880 McpServerHttp::new(name.to_string(), url.clone()).headers(
881 server
882 .headers
883 .iter()
884 .map(|(name, value)| HttpHeader::new(name.clone(), value.clone()))
885 .collect(),
886 ),
887 ))
888 }
889 McpTransport::StreamableHttp => None,
890 }
891}
892
893fn timed_request_to_error(error: TimedRequestError) -> agent_client_protocol::Error {
894 error.into()
895}
896
897async fn request_with_timeout<Req>(
898 connection: &ConnectionTo<Agent>, request: Req, timeout: Duration, agent_name: &str, operation: &str,
899 cancel: &CancelToken,
900) -> Result<Req::Response, TimedRequestError>
901where
902 Req: JsonRpcRequest,
903{
904 let request = connection.send_request(request).block_task().boxed();
905 let (watchdog, done) = watch_cancel_or_timeout(cancel.clone(), timeout);
906 let result = match select(request, watchdog.boxed()).await {
907 Either::Left((response, _)) => response.map_err(TimedRequestError::Protocol),
908 Either::Right((WatchSignal::Cancelled, _)) => Err(TimedRequestError::Cancelled),
909 Either::Right((WatchSignal::TimedOut, _)) => Err(TimedRequestError::Timeout(format!(
910 "ACP agent `{agent_name}` {operation} timed out after {} seconds",
911 timeout.as_secs()
912 ))),
913 };
914 done.store(true, Ordering::SeqCst);
915 result
916}
917
918async fn prompt_with_cancel(
919 connection: &ConnectionTo<Agent>, request: PromptRequest, session_id: SessionId, timeout: Duration,
920 agent_name: &str, cancel: &CancelToken, tx: &Sender<AgentEvent>,
921) -> Result<<PromptRequest as JsonRpcRequest>::Response, TimedRequestError> {
922 let request = connection.send_request(request).block_task().boxed();
923 let (watchdog, done) = watch_cancel_or_timeout(cancel.clone(), timeout);
924 let result = match select(request, watchdog.boxed()).await {
925 Either::Left((response, _)) => response.map_err(TimedRequestError::Protocol),
926 Either::Right((WatchSignal::Cancelled, _)) => {
927 send_session_cancel(connection, &session_id, tx);
928 Err(TimedRequestError::Cancelled)
929 }
930 Either::Right((WatchSignal::TimedOut, _)) => {
931 send_session_cancel(connection, &session_id, tx);
932 Err(TimedRequestError::Timeout(format!(
933 "ACP agent `{agent_name}` prompt timed out after {} seconds",
934 timeout.as_secs()
935 )))
936 }
937 };
938 done.store(true, Ordering::SeqCst);
939 result
940}
941
942fn send_session_cancel(connection: &ConnectionTo<Agent>, session_id: &SessionId, tx: &Sender<AgentEvent>) {
943 match connection.send_notification(CancelNotification::new(session_id.clone())) {
944 Ok(()) => {
945 let _ = tx.send(AgentEvent::Status(format!("acp: sent session/cancel for {session_id}")));
946 }
947 Err(error) => {
948 let _ = tx.send(AgentEvent::Status(format!(
949 "acp: failed to send session/cancel for {session_id}: {error}"
950 )));
951 }
952 }
953}
954
955async fn watch_receiver(rx: oneshot::Receiver<WatchSignal>) -> WatchSignal {
956 rx.await.unwrap_or(WatchSignal::Cancelled)
957}
958
959fn watch_cancel_or_timeout(
960 cancel: CancelToken, timeout: Duration,
961) -> (impl futures::Future<Output = WatchSignal>, Arc<AtomicBool>) {
962 let (tx, rx) = oneshot::channel();
963 let done = Arc::new(AtomicBool::new(false));
964 let thread_done = done.clone();
965 thread::spawn(move || {
966 let deadline = Instant::now() + timeout;
967 loop {
968 if thread_done.load(Ordering::SeqCst) {
969 return;
970 }
971 if cancel.is_cancelled() {
972 let _ = tx.send(WatchSignal::Cancelled);
973 return;
974 }
975 let now = Instant::now();
976 if now >= deadline {
977 let _ = tx.send(WatchSignal::TimedOut);
978 return;
979 }
980 thread::sleep(CANCEL_POLL_INTERVAL.min(deadline.saturating_duration_since(now)));
981 }
982 });
983 (watch_receiver(rx), done)
984}
985
986fn build_agent(agent: &AcpAgentConfig) -> Result<AcpAgent, String> {
987 let mut args = agent
988 .env
989 .iter()
990 .map(|(key, value)| format!("{key}={value}"))
991 .collect::<Vec<_>>();
992 args.push(agent.command.clone());
993 args.extend(agent.args.iter().cloned());
994 AcpAgent::from_args(args).map_err(|err| format!("failed to build ACP agent command: {err}"))
995}
996
997fn validate_admin_agent(name: &str, agent_config: &AcpAgentConfig) -> Result<(), String> {
998 if !agent_config.enabled {
999 return Err(format!("ACP agent `{name}` is disabled"));
1000 }
1001 Ok(())
1002}
1003
1004fn send_stop_reason(tx: &Sender<AgentEvent>, stop_reason: StopReason) {
1005 let event = match stop_reason {
1006 StopReason::EndTurn => AgentEvent::Finished,
1007 StopReason::Cancelled => AgentEvent::Cancelled,
1008 StopReason::MaxTokens => AgentEvent::Failed("ACP agent stopped after reaching max tokens".to_string()),
1009 StopReason::MaxTurnRequests => {
1010 AgentEvent::Failed("ACP agent stopped after reaching max turn requests".to_string())
1011 }
1012 StopReason::Refusal => AgentEvent::Failed("ACP agent refused to continue".to_string()),
1013 _ => AgentEvent::Failed("ACP agent stopped for an unsupported reason".to_string()),
1014 };
1015 let _ = tx.send(event);
1016}
1017
1018fn send(tx: &Sender<AgentEvent>, cancel: &CancelToken, event: AgentEvent) -> Option<()> {
1019 if cancel.is_cancelled() {
1020 let _ = tx.send(AgentEvent::Cancelled);
1021 return None;
1022 }
1023 tx.send(event).ok()
1024}