Skip to main content

adk_acp/
connection.rs

1//! ACP connection management.
2//!
3//! Wraps the `agent-client-protocol` SDK to manage the lifecycle of a connection
4//! to an external ACP agent process.
5
6use std::path::PathBuf;
7use std::str::FromStr;
8use std::sync::Arc;
9
10use agent_client_protocol::schema::{
11    InitializeRequest, ProtocolVersion, RequestPermissionOutcome, RequestPermissionRequest,
12    RequestPermissionResponse, SelectedPermissionOutcome,
13};
14use agent_client_protocol::{Agent, Client, ConnectionTo};
15use agent_client_protocol_tokio::AcpAgent;
16use tracing::{debug, info, warn};
17
18use crate::error::{AcpError, Result};
19use crate::permissions::{
20    PermissionDecision, PermissionOption, PermissionPolicy, PermissionRequest,
21};
22
23/// Configuration for connecting to an ACP agent.
24#[derive(Debug, Clone)]
25pub struct AcpAgentConfig {
26    /// Command to spawn the agent (e.g., "claude-code" or "codex --model o3").
27    pub command: String,
28    /// Working directory for the agent session.
29    pub working_dir: PathBuf,
30    /// Whether to auto-approve permission requests (YOLO mode).
31    /// Used by `prompt_agent()`. For fine-grained control, use `prompt_agent_with_policy()`.
32    pub auto_approve: bool,
33}
34
35impl AcpAgentConfig {
36    /// Create a new config with a command string.
37    pub fn new(command: impl Into<String>) -> Self {
38        Self {
39            command: command.into(),
40            working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
41            auto_approve: true,
42        }
43    }
44
45    /// Set the working directory.
46    pub fn working_dir(mut self, path: impl Into<PathBuf>) -> Self {
47        self.working_dir = path.into();
48        self
49    }
50
51    /// Set whether to auto-approve permission requests.
52    pub fn auto_approve(mut self, approve: bool) -> Self {
53        self.auto_approve = approve;
54        self
55    }
56}
57
58/// Send a single prompt to an ACP agent and return the response text.
59///
60/// Uses simple auto-approve/deny based on `config.auto_approve`.
61/// For fine-grained permission control, use [`prompt_agent_with_policy`].
62pub async fn prompt_agent(config: &AcpAgentConfig, prompt: &str) -> Result<String> {
63    let policy =
64        if config.auto_approve { PermissionPolicy::AutoApprove } else { PermissionPolicy::DenyAll };
65    prompt_agent_with_policy(config, prompt, Arc::new(policy)).await
66}
67
68/// Send a single prompt to an ACP agent with a custom permission policy.
69///
70/// The policy is invoked for each `session/request_permission` message from the agent.
71/// This enables HITL (human-in-the-loop) control over sensitive operations.
72///
73/// # Example
74///
75/// ```rust,ignore
76/// use adk_acp::{AcpAgentConfig, PermissionPolicy, PermissionDecision};
77/// use adk_acp::connection::prompt_agent_with_policy;
78/// use std::sync::Arc;
79///
80/// let config = AcpAgentConfig::new("kiro-cli acp");
81/// let policy = Arc::new(PermissionPolicy::Custom(Box::new(|req| {
82///     if req.title.contains("delete") {
83///         PermissionDecision::deny()
84///     } else {
85///         PermissionDecision::allow_once()
86///     }
87/// })));
88///
89/// let response = prompt_agent_with_policy(&config, "Refactor main.rs", policy).await?;
90/// ```
91pub async fn prompt_agent_with_policy(
92    config: &AcpAgentConfig,
93    prompt: &str,
94    policy: Arc<PermissionPolicy>,
95) -> Result<String> {
96    info!(command = %config.command, cwd = %config.working_dir.display(), "spawning ACP agent");
97
98    let agent = AcpAgent::from_str(&config.command).map_err(|e| {
99        AcpError::InvalidConfig(format!("invalid command '{}': {e}", config.command))
100    })?;
101
102    let prompt_text = prompt.to_string();
103    let working_dir = config.working_dir.clone();
104    let policy_clone = policy.clone();
105
106    let result: std::result::Result<String, agent_client_protocol::Error> = Client
107        .builder()
108        .on_receive_request(
109            async move |request: RequestPermissionRequest, responder, _cx: ConnectionTo<Agent>| {
110                // Convert SDK permission request to our domain type
111                let title = request
112                    .options
113                    .first()
114                    .map(|o| o.name.to_string())
115                    .unwrap_or_else(|| "Unknown operation".to_string());
116
117                let perm_request = PermissionRequest {
118                    title: title.clone(),
119                    options: request
120                        .options
121                        .iter()
122                        .map(|o| PermissionOption {
123                            id: o.option_id.to_string(),
124                            name: o.name.to_string(),
125                        })
126                        .collect(),
127                };
128
129                // Evaluate the policy
130                let decision = policy_clone.decide(&perm_request);
131
132                match &decision {
133                    PermissionDecision::Allow(option_id) => {
134                        debug!(title = %title, decision = %decision, "ACP permission granted");
135                        responder.respond(RequestPermissionResponse::new(
136                            RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new(
137                                option_id.clone(),
138                            )),
139                        ))
140                    }
141                    PermissionDecision::Deny => {
142                        warn!(title = %title, "ACP permission DENIED by policy");
143                        responder.respond(RequestPermissionResponse::new(
144                            RequestPermissionOutcome::Cancelled,
145                        ))
146                    }
147                }
148            },
149            agent_client_protocol::on_receive_request!(),
150        )
151        .connect_with(agent, |connection: ConnectionTo<Agent>| async move {
152            // Initialize
153            connection
154                .send_request(InitializeRequest::new(ProtocolVersion::V1))
155                .block_task()
156                .await?;
157
158            // Create session, send prompt, and collect response
159            let response_text = connection
160                .build_session(&working_dir)
161                .block_task()
162                .run_until(async |mut session| {
163                    session.send_prompt(&prompt_text)?;
164                    let text = session.read_to_string().await?;
165                    Ok(text)
166                })
167                .await?;
168
169            Ok(response_text)
170        })
171        .await;
172
173    result.map_err(|e| AcpError::Protocol(e.to_string()))
174}