use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use agent_client_protocol::schema::{
InitializeRequest, ProtocolVersion, RequestPermissionOutcome, RequestPermissionRequest,
RequestPermissionResponse, SelectedPermissionOutcome,
};
use agent_client_protocol::{Agent, Client, ConnectionTo};
use agent_client_protocol_tokio::AcpAgent;
use tracing::{debug, info, warn};
use crate::error::{AcpError, Result};
use crate::permissions::{
PermissionDecision, PermissionOption, PermissionPolicy, PermissionRequest,
};
#[derive(Debug, Clone)]
pub struct AcpAgentConfig {
pub command: String,
pub working_dir: PathBuf,
pub auto_approve: bool,
}
impl AcpAgentConfig {
pub fn new(command: impl Into<String>) -> Self {
Self {
command: command.into(),
working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
auto_approve: true,
}
}
pub fn working_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.working_dir = path.into();
self
}
pub fn auto_approve(mut self, approve: bool) -> Self {
self.auto_approve = approve;
self
}
}
pub async fn prompt_agent(config: &AcpAgentConfig, prompt: &str) -> Result<String> {
let policy =
if config.auto_approve { PermissionPolicy::AutoApprove } else { PermissionPolicy::DenyAll };
prompt_agent_with_policy(config, prompt, Arc::new(policy)).await
}
pub async fn prompt_agent_with_policy(
config: &AcpAgentConfig,
prompt: &str,
policy: Arc<PermissionPolicy>,
) -> Result<String> {
info!(command = %config.command, cwd = %config.working_dir.display(), "spawning ACP agent");
let agent = AcpAgent::from_str(&config.command).map_err(|e| {
AcpError::InvalidConfig(format!("invalid command '{}': {e}", config.command))
})?;
let prompt_text = prompt.to_string();
let working_dir = config.working_dir.clone();
let policy_clone = policy.clone();
let result: std::result::Result<String, agent_client_protocol::Error> = Client
.builder()
.on_receive_request(
async move |request: RequestPermissionRequest, responder, _cx: ConnectionTo<Agent>| {
let title = request
.options
.first()
.map(|o| o.name.to_string())
.unwrap_or_else(|| "Unknown operation".to_string());
let perm_request = PermissionRequest {
title: title.clone(),
options: request
.options
.iter()
.map(|o| PermissionOption {
id: o.option_id.to_string(),
name: o.name.to_string(),
})
.collect(),
};
let decision = policy_clone.decide(&perm_request);
match &decision {
PermissionDecision::Allow(option_id) => {
debug!(title = %title, decision = %decision, "ACP permission granted");
responder.respond(RequestPermissionResponse::new(
RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new(
option_id.clone(),
)),
))
}
PermissionDecision::Deny => {
warn!(title = %title, "ACP permission DENIED by policy");
responder.respond(RequestPermissionResponse::new(
RequestPermissionOutcome::Cancelled,
))
}
}
},
agent_client_protocol::on_receive_request!(),
)
.connect_with(agent, |connection: ConnectionTo<Agent>| async move {
connection
.send_request(InitializeRequest::new(ProtocolVersion::V1))
.block_task()
.await?;
let response_text = connection
.build_session(&working_dir)
.block_task()
.run_until(async |mut session| {
session.send_prompt(&prompt_text)?;
let text = session.read_to_string().await?;
Ok(text)
})
.await?;
Ok(response_text)
})
.await;
result.map_err(|e| AcpError::Protocol(e.to_string()))
}