use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use async_trait::async_trait;
use serde::Deserialize;
use tokio::sync::mpsc;
use crate::adapter::{
Adapter, ConnectRequest, DriverCommand, DriverConnection, DriverEvent, DriverInfo,
};
use crate::agent::{
AgentDetails, AuthKind, AuthStatus, Capabilities, Capability, ConfigOption,
SessionConfiguration,
};
use crate::error::AgentError;
use crate::event::{
CompletionSource, EventKind, MessageId, PermissionChoice, PermissionRequest, Request,
RequestId, StopReason, ToolId, ToolInput, ToolKind, ToolStatus, ToolUpdate,
};
#[derive(Debug, Clone, Deserialize)]
#[allow(clippy::large_enum_variant)]
pub enum Step {
Emit(EventKind),
AwaitAnswer,
End(StopReason),
Die,
Sleep(u64),
Repeat {
times: u32,
steps: Vec<Step>,
},
}
#[derive(Debug, Clone, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Script {
pub turns: VecDeque<Vec<Step>>,
pub steer: bool,
pub steer_rejects: bool,
pub steer_ack: bool,
pub ignore_close: bool,
pub deterministic: bool,
pub deterministic_agent: bool,
pub buffer: usize,
pub stale_before_ack: Option<EventKind>,
pub compact: bool,
pub permissions: bool,
pub options: Vec<ConfigOption>,
}
impl Default for Script {
fn default() -> Self {
Self {
turns: VecDeque::new(),
steer: false,
steer_rejects: false,
steer_ack: true,
ignore_close: false,
deterministic: true,
deterministic_agent: true,
buffer: 64,
stale_before_ack: None,
compact: false,
permissions: true,
options: Vec::new(),
}
}
}
impl Script {
pub fn turn(mut self, steps: Vec<Step>) -> Self {
self.turns.push_back(steps);
self
}
}
pub struct MockAdapter {
script: Script,
sent: Arc<AtomicUsize>,
}
impl MockAdapter {
pub fn new(script: Script) -> Self {
Self {
script,
sent: Arc::new(AtomicUsize::new(0)),
}
}
pub fn permission_flow() -> Self {
Self::new(Script::default().turn(vec![
Step::Emit(text("m1", "Let me check. ")),
Step::Emit(permission("r1")),
Step::AwaitAnswer,
Step::Emit(text("m1", "Done.")),
Step::End(completed()),
]))
}
pub fn sent(&self) -> Arc<AtomicUsize> {
Arc::clone(&self.sent)
}
}
#[async_trait]
impl Adapter for MockAdapter {
async fn connect(&self, _request: ConnectRequest) -> Result<DriverConnection, AgentError> {
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
let (ev_tx, ev_rx) = mpsc::channel(self.script.buffer);
tokio::spawn(drive(
self.script.clone(),
cmd_rx,
ev_tx,
Arc::clone(&self.sent),
));
Ok(DriverConnection {
info: info(&self.script, &initial_configuration(&self.script)),
commands: cmd_tx,
events: ev_rx,
})
}
}
async fn drive(
mut script: Script,
mut commands: mpsc::UnboundedReceiver<DriverCommand>,
events: mpsc::Sender<DriverEvent>,
sent: Arc<AtomicUsize>,
) {
let mut steps: VecDeque<Step> = VecDeque::new();
let mut waiting = false;
let mut turn_open = false;
let mut configuration = initial_configuration(&script);
let send = |ev: DriverEvent| {
let events = events.clone();
let sent = Arc::clone(&sent);
async move {
let ok = events.send(ev).await.is_ok();
if ok {
sent.fetch_add(1, Ordering::SeqCst);
}
ok
}
};
loop {
while !waiting {
let Some(step) = steps.pop_front() else { break };
let ok = match step {
Step::Emit(kind) => send(DriverEvent::event(kind)).await,
Step::AwaitAnswer => {
waiting = true;
true
}
Step::End(stop) => {
turn_open = false;
send(DriverEvent::TurnEnded(stop)).await
}
Step::Die => {
send(DriverEvent::Exited {
status: "9".into(),
stderr: "mock died".into(),
})
.await;
return;
}
Step::Sleep(ms) => {
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
true
}
Step::Repeat {
times,
steps: block,
} => {
for _ in 0..times {
for step in block.iter().rev() {
steps.push_front(step.clone());
}
}
true
}
};
if !ok {
return;
}
}
let Some(cmd) = commands.recv().await else {
return;
};
match cmd {
DriverCommand::StartTurn { .. } => {
if let Some(kind) = script.stale_before_ack.clone()
&& !send(DriverEvent::event(kind)).await
{
return;
}
if !send(DriverEvent::TurnAck).await {
return;
}
steps = script.turns.pop_front().unwrap_or_default().into();
turn_open = true;
}
DriverCommand::Steer { .. } => {
if script.steer_ack
&& !send(DriverEvent::Steered(script.steer && !script.steer_rejects)).await
{
return;
}
}
DriverCommand::Answer { .. } => waiting = false,
DriverCommand::Cancel if turn_open => {
steps.clear();
waiting = false;
turn_open = false;
if !send(DriverEvent::TurnEnded(StopReason::Cancelled)).await {
return;
}
}
DriverCommand::Compact => {
if !send(DriverEvent::event(EventKind::ContextCompacted)).await
|| !send(DriverEvent::TurnEnded(StopReason::Completed {
source: CompletionSource::Protocol,
}))
.await
{
return;
}
}
DriverCommand::Configure(id, value) => {
configuration.options.insert(id, value);
if !send(DriverEvent::InfoChanged(info(&script, &configuration))).await {
return;
}
}
DriverCommand::Cancel | DriverCommand::Rollback(..) => {}
DriverCommand::Close if script.ignore_close => {}
DriverCommand::Close => return,
}
}
}
fn info(script: &Script, configuration: &SessionConfiguration) -> DriverInfo {
let mut caps = vec![Capability::Questions];
if script.permissions {
caps.push(Capability::Permissions);
}
if script.steer {
caps.push(Capability::Steer);
}
if script.compact {
caps.push(Capability::Compact);
}
let config_options = script
.options
.iter()
.cloned()
.map(|mut option| {
if let Some(value) = configuration.options.get(&option.id) {
option.current = Some(value.clone());
}
option
})
.collect();
DriverInfo {
details: AgentDetails {
version: Some("mock".into()),
auth: AuthStatus::Authenticated {
kind: AuthKind::ApiKey,
account: None,
},
capabilities: Capabilities::new(caps),
config_options,
commands: Vec::new(),
},
configuration: configuration.clone(),
resume_token: None,
title: None,
deterministic_turn_end: script.deterministic,
deterministic_agent_turn_end: script.deterministic_agent,
tools_disabled: false,
effort_wire: None,
}
}
fn initial_configuration(script: &Script) -> SessionConfiguration {
let options = script
.options
.iter()
.filter_map(|o| Some((o.id.clone(), o.current.clone()?)))
.collect();
SessionConfiguration { options }
}
pub fn text(message: &str, text: &str) -> EventKind {
EventKind::TextDelta {
message_id: MessageId::new(message),
text: text.into(),
}
}
pub fn tool(id: &str, status: ToolStatus) -> EventKind {
EventKind::ToolUpdated(ToolUpdate {
id: ToolId::new(id),
kind: ToolKind::Execute,
title: "cargo test".into(),
status,
input: ToolInput::Command {
command: "cargo test".into(),
cwd: None,
},
output: None,
diffs: Vec::new(),
locations: Vec::new(),
raw: None,
})
}
pub fn permission(id: &str) -> EventKind {
let EventKind::ToolUpdated(tool) = tool("tool-1", ToolStatus::Pending) else {
unreachable!()
};
EventKind::RequestOpened(Request::Permission(PermissionRequest {
id: RequestId::new(id),
tool,
options: vec![PermissionChoice::AllowOnce, PermissionChoice::DenyOnce],
detail: None,
}))
}
pub fn completed() -> StopReason {
StopReason::Completed {
source: CompletionSource::Protocol,
}
}