Skip to main content

thndrs_lib/core/acp/
runner.rs

1//! ACP run lifecycle.
2
3use 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/// Handle for a client-side ACP run.
50#[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    /// Build an ACP run handle for `acp:<name>`.
63    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    /// Attach effective MCP config that should be offered to the ACP agent.
68    pub fn with_mcp_config(mut self, mcp_config: McpConfig) -> Self {
69        self.mcp_config = Some(mcp_config);
70        self
71    }
72
73    /// Attach redacted MCP config loader diagnostics.
74    pub fn with_mcp_diagnostics(mut self, diagnostics: Vec<String>) -> Self {
75        self.mcp_diagnostics = diagnostics;
76        self
77    }
78
79    /// Spawn an ACP run and return the normal agent event receiver.
80    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/// ACP session listed by an external agent.
88#[derive(Clone, Debug, Eq, PartialEq)]
89pub struct ListedSession {
90    /// Opaque external ACP session id.
91    pub session_id: String,
92    /// Workspace root reported by the ACP agent.
93    pub cwd: PathBuf,
94    /// Additional workspace roots reported by the ACP agent.
95    pub additional_directories: Vec<PathBuf>,
96    /// Human-readable title reported by the ACP agent.
97    pub title: Option<String>,
98    /// Last activity timestamp reported by the ACP agent.
99    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
126/// Log out of an ACP agent when the agent advertises logout support.
127pub 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
134/// List agent-owned ACP sessions when the agent advertises list support.
135pub 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
140/// Load an agent-owned ACP session and return replayed session update events.
141pub 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
148/// Resume an agent-owned ACP session without replaying its history.
149pub 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
156/// Close an agent-owned ACP session when the agent advertises close support.
157pub 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}