mod client;
mod installation;
pub use client::Client;
use crate::agent::{InferenceCapabilities, InferencePermissionPolicy};
use acorn_cmd::args;
use acorn_core::util::SemanticVersion;
use acorn_macros::With;
use alloc::{collections::BTreeMap, string::String, vec::Vec};
use bon::Builder;
use core::time::Duration;
use schemars::JsonSchema;
use serde::Serialize;
use std::{
ffi::OsString,
path::{Path, PathBuf},
};
pub const OPENCODE_AGENT_ID: &str = "opencode";
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("ACP agent '{requested}' was requested, but '{configured}' is configured")]
AgentMismatch {
configured: String,
requested: String,
},
#[error("ACP agent '{agent_id}' exited before completion — {message}")]
ChildExit {
agent_id: String,
message: String,
},
#[error("ACP agent '{agent_id}' executable was not found: {}", .executable.display())]
ExecutableNotFound {
agent_id: String,
executable: PathBuf,
},
#[error("ACP agent '{agent_id}' executable is not usable: {}", .executable.display())]
ExecutableNotUsable {
agent_id: String,
executable: PathBuf,
},
#[error("ACP agent '{agent_id}' configuration is invalid — {message}")]
InvalidConfiguration {
agent_id: String,
message: String,
},
#[error("ACP agent '{agent_id}' did not advertise requested model '{model}'")]
ModelUnavailable {
agent_id: String,
model: String,
},
#[error("ACP agent '{agent_id}' initialization failed — {message}")]
Negotiation {
agent_id: String,
message: String,
},
#[error("ACP inference through '{agent_id}' is unavailable in offline mode")]
Offline {
agent_id: String,
},
#[error("ACP agent '{agent_id}' requested a permission denied by policy")]
PermissionDenied {
agent_id: String,
},
#[error("ACP agent '{agent_id}' process failed — {message}")]
Process {
agent_id: String,
message: String,
},
#[error("ACP agent '{agent_id}' protocol operation failed — {message}")]
Protocol {
agent_id: String,
message: String,
},
#[error("ACP agent '{agent_id}' session failed — {message}")]
Session {
agent_id: String,
message: String,
},
#[error("ACP agent '{agent_id}' exceeded the {timeout:?} timeout")]
Timeout {
agent_id: String,
timeout: Duration,
},
#[error("ACP agent '{agent_id}' version probe failed — {message}")]
VersionProbe {
agent_id: String,
message: String,
},
#[error("ACP agent '{agent_id}' version probe exceeded {timeout:?}")]
VersionProbeTimeout {
agent_id: String,
timeout: Duration,
},
}
#[derive(Clone, Debug, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AgentInstallation {
pub agent_id: String,
pub executable: PathBuf,
pub version_raw: String,
pub version: Option<SemanticVersion>,
}
#[derive(Builder, Clone, Debug, Eq, PartialEq, With)]
pub struct AgentProcessConfig {
#[builder(default)]
#[with(skip)]
acp_arguments: Vec<OsString>,
#[builder(into)]
agent_id: String,
#[builder(default)]
#[with(skip)]
environment: BTreeMap<OsString, OsString>,
#[builder(into)]
executable: PathBuf,
#[builder(default = InferencePermissionPolicy::DenyMutation)]
permission_policy: InferencePermissionPolicy,
#[builder(default = Duration::from_secs(120))]
timeout: Duration,
#[builder(default = args!["--version"])]
#[with(skip)]
version_arguments: Vec<OsString>,
#[builder(into)]
working_directory: PathBuf,
}
#[derive(Clone, Debug, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct AuthenticationMethod {
pub id: String,
pub name: String,
pub terminal: bool,
}
#[derive(Clone, Debug, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ProtocolReadiness {
pub agent_name: Option<String>,
pub agent_version: Option<String>,
pub authentication_methods: Vec<AuthenticationMethod>,
pub capabilities: InferenceCapabilities,
pub installation: AgentInstallation,
pub protocol_version: u16,
}
impl AgentProcessConfig {
pub fn acp_arguments(&self) -> &[OsString] {
&self.acp_arguments
}
pub fn agent_id(&self) -> &str {
&self.agent_id
}
pub fn environment(&self) -> &BTreeMap<OsString, OsString> {
&self.environment
}
pub fn executable(&self) -> &Path {
&self.executable
}
pub fn new(agent_id: impl Into<String>, executable: impl Into<PathBuf>, working_directory: impl Into<PathBuf>) -> Self {
Self::builder()
.agent_id(agent_id)
.executable(executable)
.working_directory(working_directory)
.build()
}
pub fn opencode(executable: Option<PathBuf>, working_directory: PathBuf) -> Self {
Self::builder()
.agent_id(OPENCODE_AGENT_ID)
.executable(executable.unwrap_or_else(|| PathBuf::from(OPENCODE_AGENT_ID)))
.working_directory(working_directory)
.acp_arguments(args!["acp"])
.build()
}
pub const fn permission_policy(&self) -> InferencePermissionPolicy {
self.permission_policy
}
pub const fn timeout(&self) -> Duration {
self.timeout
}
pub fn version_arguments(&self) -> &[OsString] {
&self.version_arguments
}
pub fn with_acp_arguments<I, S>(self, arguments: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<OsString>,
{
Self {
acp_arguments: arguments.into_iter().map(Into::into).collect(),
..self
}
}
pub fn with_environment<I, K, V>(self, environment: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<OsString>,
V: Into<OsString>,
{
Self {
environment: environment.into_iter().map(|(key, value)| (key.into(), value.into())).collect(),
..self
}
}
pub fn with_version_arguments<I, S>(self, arguments: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<OsString>,
{
Self {
version_arguments: arguments.into_iter().map(Into::into).collect(),
..self
}
}
pub fn working_directory(&self) -> &Path {
&self.working_directory
}
}
#[cfg(test)]
mod tests;