use std::num::NonZeroU32;
use async_trait::async_trait;
use serde_json::{Value, json};
use tokio::io::AsyncWriteExt;
use tokio::sync::mpsc;
use crate::agent::{
AgentDetails, AgentInstallation, ConfigId, ConfigValue, Input, 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 attach;
pub(crate) mod claude;
pub(crate) mod codex;
#[cfg(test)]
mod conformance;
#[cfg(test)]
pub(crate) mod mock;
#[derive(Debug)]
pub(crate) enum DriverCommand {
StartTurn {
input: Input,
},
Steer {
input: Input,
},
Answer {
request: RequestId,
answer: Answer,
},
Configure(ConfigId, ConfigValue),
Rollback(NonZeroU32, RollbackScope),
Cancel,
Close,
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum DriverEvent {
Event {
kind: EventKind,
parent_tool_id: Option<ToolId>,
extensions: Extensions,
},
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(crate) fn apply_selection(
info: &mut DriverInfo,
id: &crate::agent::ConfigId,
value: &crate::agent::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 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
))),
}
}
#[derive(Clone)]
pub(crate) struct WireRecorder {
lines: mpsc::UnboundedSender<Vec<u8>>,
}
impl WireRecorder {
pub(crate) async fn for_session(
options: &SessionOptions,
events: &mpsc::Sender<DriverEvent>,
) -> Option<Self> {
let path = options.record_wire.as_deref()?;
let events = events.clone();
let file = match tokio::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.await
{
Ok(file) => file,
Err(e) => {
warn(&events, format!("wire recording is off: {e}")).await;
return None;
}
};
let (lines, mut rx) = mpsc::unbounded_channel::<Vec<u8>>();
tokio::spawn(async move {
let mut file = file;
while let Some(bytes) = rx.recv().await {
if let Err(e) = append(&mut file, &bytes).await {
warn(&events, format!("wire recording stopped: {e}")).await;
break; }
}
});
Some(Self { lines })
}
pub(crate) fn record(&self, dir: &'static str, frame: &Value) {
let mut line = json!({ "dir": dir, "frame": frame }).to_string();
line.push('\n');
let _ = self.lines.send(line.into_bytes());
}
}
async fn append(file: &mut tokio::fs::File, bytes: &[u8]) -> std::io::Result<()> {
file.write_all(bytes).await?;
file.flush().await
}
async fn warn(events: &mpsc::Sender<DriverEvent>, message: String) {
let _ = events
.send(DriverEvent::event(EventKind::Diagnostic(Diagnostic {
level: DiagnosticLevel::Warning,
message,
})))
.await;
}
pub(crate) fn login_methods(installation: &AgentInstallation) -> Vec<crate::agent::LoginMethod> {
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()
}
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 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
}
#[derive(Clone)]
pub(crate) struct ConnectRequest {
pub installation: AgentInstallation,
pub options: SessionOptions,
}
pub(crate) struct DriverConnection {
pub info: DriverInfo,
pub commands: mpsc::Sender<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()))
}
}