use std::num::NonZeroU32;
use std::path::Path;
use std::time::Duration;
use async_trait::async_trait;
use serde_json::Value;
use tokio::sync::mpsc;
use crate::agent::{
AgentDetails, AgentInstallation, ConfigChoice, ConfigId, ConfigKind, ConfigOption, ConfigValue,
Input, LoginMethod, ResumeToken, RollbackScope, SessionConfiguration, SessionOptions,
};
use crate::error::AgentError;
use crate::event::{
Answer, Diagnostic, DiagnosticLevel, EventKind, Extensions, RequestId, StopReason, ToolId,
};
pub(crate) mod acp;
pub(crate) mod antigravity;
pub(crate) mod attach;
pub(crate) mod claude;
pub(crate) mod codex;
#[cfg(test)]
mod conformance;
#[cfg(any(test, feature = "mock"))]
pub mod mock;
pub(crate) mod opencode;
pub(crate) mod pi;
pub(crate) mod wire;
pub(crate) use wire::{FRAME_BUFFER, LineWire, WireRecorder};
pub(crate) const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) const CLOSE_GRACE: Duration = Duration::from_secs(2);
pub(crate) const OUTPUT_CAP: usize = 16 * 1024;
#[derive(Debug)]
pub(crate) enum DriverCommand {
StartTurn {
input: Input,
},
Steer {
input: Input,
},
Answer {
request: RequestId,
answer: Answer,
},
Configure(ConfigId, ConfigValue),
Rollback(NonZeroU32, RollbackScope),
Compact,
Cancel,
Close,
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum DriverEvent {
Event {
kind: EventKind,
parent_tool_id: Option<ToolId>,
extensions: Extensions,
},
TurnAck,
TurnEnded(StopReason),
Steered(bool),
InfoChanged(DriverInfo),
AuthLost {
login: Vec<crate::agent::LoginMethod>,
},
Exited { status: String, stderr: String },
}
impl DriverEvent {
pub(crate) fn event(kind: EventKind) -> Self {
DriverEvent::Event {
kind,
parent_tool_id: None,
extensions: Extensions::new(),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct DriverInfo {
pub details: AgentDetails,
pub configuration: SessionConfiguration,
pub resume_token: Option<ResumeToken>,
pub title: Option<String>,
pub deterministic_turn_end: bool,
pub deterministic_agent_turn_end: bool,
pub tools_disabled: bool,
pub effort_wire: Option<String>,
}
#[derive(Clone)]
pub(crate) struct ConnectRequest {
pub installation: AgentInstallation,
pub options: SessionOptions,
}
pub(crate) struct DriverConnection {
pub info: DriverInfo,
pub commands: mpsc::UnboundedSender<DriverCommand>,
pub events: mpsc::Receiver<DriverEvent>,
}
#[async_trait]
pub(crate) trait Adapter: Send + Sync {
async fn connect(&self, request: ConnectRequest) -> Result<DriverConnection, AgentError>;
async fn plan_usage(
&self,
installation: &AgentInstallation,
) -> Result<crate::event::PlanUsage, AgentError> {
let _ = installation;
Err(AgentError::UnsupportedFeature("plan usage".into()))
}
}
pub(crate) struct Gone;
impl From<std::io::Error> for Gone {
fn from(_: std::io::Error) -> Self {
Gone
}
}
#[derive(Clone)]
pub(crate) struct Emitter(mpsc::Sender<DriverEvent>);
impl Emitter {
pub(crate) fn new(events: mpsc::Sender<DriverEvent>) -> Self {
Self(events)
}
pub(crate) async fn send(&self, event: DriverEvent) -> Result<(), Gone> {
self.0.send(event).await.map_err(|_| Gone)
}
pub(crate) async fn event(&self, kind: EventKind) -> Result<(), Gone> {
self.send(DriverEvent::event(kind)).await
}
pub(crate) async fn content(
&self,
kind: EventKind,
parent_tool_id: Option<ToolId>,
extensions: Extensions,
) -> Result<(), Gone> {
self.send(DriverEvent::Event {
kind,
parent_tool_id,
extensions,
})
.await
}
pub(crate) async fn diagnostic(
&self,
level: DiagnosticLevel,
message: impl Into<String>,
) -> Result<(), Gone> {
self.event(EventKind::Diagnostic(Diagnostic {
level,
message: message.into(),
}))
.await
}
pub(crate) async fn exited(&self, child: &mut crate::process::Child) {
let status = child.exit_status(CLOSE_GRACE).await;
let stderr = child.stderr_tail();
self.send(DriverEvent::Exited { status, stderr }).await.ok();
}
}
pub(crate) fn apply_selection(info: &mut DriverInfo, id: &ConfigId, value: &ConfigValue) -> bool {
let stored = info.configuration.options.get(id);
if stored == Some(value) {
return false;
}
info.configuration.options.insert(id.clone(), value.clone());
if let Some(option) = info.details.config_options.iter_mut().find(|o| &o.id == id) {
option.current = Some(value.clone());
}
true
}
pub(crate) fn selected(info: &DriverInfo, id: &str) -> Option<String> {
match info.configuration.options.get(&ConfigId::new(id)) {
Some(ConfigValue::Text(value)) => Some(value.clone()),
_ => None,
}
}
pub(crate) fn offers(info: &DriverInfo, id: &str, value: &ConfigValue) -> bool {
info.details.config_options.iter().any(|o| {
o.id.as_str() == id
&& matches!((&o.kind, value), (ConfigKind::Select { choices }, ConfigValue::Text(v))
if choices.iter().any(|c| &c.value == v))
})
}
pub(crate) fn set_select_option(
info: &mut DriverInfo,
id: &str,
name: &str,
category: &str,
choices: Vec<ConfigChoice>,
current: Option<String>,
) {
let id = ConfigId::new(id);
info.details.config_options.retain(|o| o.id != id);
info.configuration.options.remove(&id);
if choices.is_empty() {
return;
}
let current = current
.filter(|c| choices.iter().any(|choice| &choice.value == c))
.map(ConfigValue::Text);
if let Some(current) = ¤t {
info.configuration
.options
.insert(id.clone(), current.clone());
}
info.details.config_options.push(ConfigOption {
id,
name: name.into(),
category: Some(category.into()),
kind: ConfigKind::Select { choices },
current,
live: true,
});
}
pub(crate) fn set_effort_option(
info: &mut DriverInfo,
choices: Vec<ConfigChoice>,
current: Option<String>,
) {
set_select_option(
info,
"effort",
"Reasoning effort",
"thought_level",
choices,
current,
);
}
pub(crate) fn level_choices<'a>(levels: impl IntoIterator<Item = &'a str>) -> Vec<ConfigChoice> {
levels
.into_iter()
.map(|level| ConfigChoice {
value: level.to_owned(),
label: level.to_owned(),
description: None,
})
.collect()
}
pub(crate) fn set_fast_option(info: &mut DriverInfo, current: Option<bool>, live: bool) {
let id = ConfigId::new("fast");
info.details.config_options.retain(|option| option.id != id);
info.configuration.options.remove(&id);
if let Some(current) = current {
let value = ConfigValue::Bool(current);
let position = info
.details
.config_options
.iter()
.position(|option| option.id.as_str() == "model")
.map_or(0, |index| index + 1);
info.details.config_options.insert(
position,
ConfigOption {
id: id.clone(),
name: "Fast mode".into(),
category: Some("speed".into()),
kind: ConfigKind::Boolean,
current: Some(value.clone()),
live,
},
);
info.configuration.options.insert(id, value);
}
}
pub(crate) fn plan_entries(todos: &Value, key: &str) -> Vec<crate::event::PlanEntry> {
use crate::event::{PlanEntry, PlanStatus};
todos
.as_array()
.into_iter()
.flatten()
.map(|todo| PlanEntry {
text: todo[key].as_str().unwrap_or_default().to_owned(),
status: match todo["status"].as_str().unwrap_or_default() {
"in_progress" | "inProgress" => PlanStatus::InProgress,
"completed" => PlanStatus::Completed,
_ => PlanStatus::Pending,
},
})
.collect()
}
pub(crate) fn config_home_env(
installation: &AgentInstallation,
options: &SessionOptions,
) -> Result<Vec<(String, String)>, AgentError> {
let Some(dir) = &options.config_home else {
return Ok(Vec::new());
};
match crate::catalog::config_home_env(installation.id.as_str()) {
Some(var) => Ok(vec![(var.to_owned(), dir.to_string_lossy().into_owned())]),
None => Err(AgentError::InvalidConfiguration(format!(
"{} has no config-home environment variable to isolate its login",
installation.id
))),
}
}
pub(crate) fn login_methods(
installation: &AgentInstallation,
options: Option<&SessionOptions>,
) -> Vec<LoginMethod> {
let login = crate::catalog::PROFILES
.iter()
.find(|p| p.id == installation.id.as_str())
.map(|p| crate::discovery::login_methods(p, &installation.executable_path))
.unwrap_or_default();
let env = options
.and_then(|o| config_home_env(installation, o).ok())
.unwrap_or_default();
login_in(login, &env)
}
pub(crate) fn login_in(mut login: Vec<LoginMethod>, env: &[(String, String)]) -> Vec<LoginMethod> {
for method in &mut login {
if let LoginMethod::Terminal { env: vars, .. } = method {
vars.extend(env.iter().cloned());
}
}
login
}
pub(crate) fn with_stderr(error: AgentError, child: &crate::process::Child) -> AgentError {
let stderr = child.stderr_tail();
match (error, stderr.is_empty()) {
(AgentError::ProtocolFailed(message), false) => {
AgentError::ProtocolFailed(format!("{message}: {stderr}"))
}
(error, _) => error,
}
}
pub(crate) fn auth_hinted(
error: AgentError,
profile: Option<&crate::catalog::AgentProfile>,
exe: &Path,
env: &[(String, String)],
) -> AgentError {
if let AgentError::AuthRequired { login } = error {
return AgentError::AuthRequired {
login: login_in(login, env),
};
}
let Some(profile) = profile else { return error };
if !matches!(
error,
AgentError::ProtocolFailed(_) | AgentError::ProcessExited { .. }
) {
return error;
}
let text = error.to_string().to_lowercase();
if !profile
.auth_error_hints
.iter()
.any(|h| text.contains(&h.to_lowercase()))
{
return error;
}
let exe = match &profile.upgrade {
Some(u) if crate::discovery::is_named(exe, u.cli) => Path::new(profile.cli),
_ => exe,
};
let mut login = crate::discovery::login_methods(profile, exe);
if !login
.iter()
.any(|m| matches!(m, LoginMethod::Terminal { .. }))
{
login.insert(
0,
LoginMethod::Terminal {
description: format!("{} logs in on launch; run it in a terminal", profile.name),
command: vec![exe.to_string_lossy().into_owned()],
env: std::collections::BTreeMap::new(),
},
);
}
AgentError::AuthRequired {
login: login_in(login, env),
}
}
pub(crate) fn cap(mut s: String, at: usize) -> String {
if s.len() > at {
let mut end = at;
while !s.is_char_boundary(end) {
end -= 1;
}
s.truncate(end);
}
s
}