aether_evals/agents/
fake_agent.rs1use super::agent::{Agent, AgentRunResult, RunError};
2use crate::Task;
3use aether_core::events::{AgentEvent, StreamState, TurnOutcome};
4use async_stream::try_stream;
5use futures::Stream;
6use std::path::{Path, PathBuf};
7use tokio::fs::{create_dir_all, write};
8
9#[derive(Clone)]
10pub struct FakeAgent {
11 events: Vec<AgentEvent>,
12 file_writes: Vec<(PathBuf, String)>,
13 workspace: Option<PathBuf>,
14}
15
16impl FakeAgent {
17 pub fn new(events: Vec<AgentEvent>) -> Self {
18 Self { events, file_writes: Vec::new(), workspace: None }
19 }
20
21 pub fn success() -> Self {
22 Self::new(vec![
23 AgentEvent::text("fake_1", "Task completed successfully", StreamState::Complete),
24 AgentEvent::turn_ended(TurnOutcome::Completed),
25 ])
26 }
27
28 pub fn with_tool_call(tool_name: impl Into<String>, result: impl Into<String>) -> Self {
29 Self::new(vec![
30 AgentEvent::tool_result("fake_call_1", &tool_name.into(), "{}", &result.into()),
31 AgentEvent::text("fake_2", "Task completed using tools", StreamState::Complete),
32 AgentEvent::turn_ended(TurnOutcome::Completed),
33 ])
34 }
35
36 pub fn writes_file(path: impl Into<PathBuf>, contents: impl Into<String>) -> Self {
37 Self::success().with_file_write(path, contents)
38 }
39
40 pub fn with_file_write(mut self, path: impl Into<PathBuf>, contents: impl Into<String>) -> Self {
41 self.file_writes.push((path.into(), contents.into()));
42 self
43 }
44
45 pub fn with_workspace(mut self, workspace: impl AsRef<Path>) -> Self {
46 self.workspace = Some(workspace.as_ref().to_path_buf());
47 self
48 }
49}
50
51impl Agent for FakeAgent {
52 fn run(&self, _task: Task) -> impl Stream<Item = AgentRunResult> + Send + '_ {
53 try_stream! {
54 if !self.file_writes.is_empty() && self.workspace.is_none() {
55 Err(RunError::ConfigurationError("FakeAgent file writes require a bound workspace".to_string()))?;
56 }
57
58 for (path, contents) in &self.file_writes {
59 let file_path = self.workspace.as_ref().expect("workspace checked above").join(path);
60 if let Some(parent) = file_path.parent() {
61 create_dir_all(parent).await.map_err(|error| {
62 RunError::ExecutionFailed(format!("failed to create fake file parent: {error}"))
63 })?;
64 }
65 write(&file_path, contents)
66 .await
67 .map_err(|error| RunError::ExecutionFailed(format!("failed to write fake file: {error}")))?;
68 }
69
70 for event in &self.events {
71 yield event.clone();
72 }
73 }
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use aether_core::events::{MessageEvent, TurnEvent};
81
82 #[tokio::test]
83 async fn fake_agent_success_sends_done() {
84 let agent = FakeAgent::success();
85 let transcript = crate::Transcript::from_stream(agent.run(Task::new("test task"))).await.unwrap();
86 let messages = transcript.events();
87
88 assert!(matches!(messages.first(), Some(AgentEvent::Message(MessageEvent::Text { .. }))));
89 assert!(matches!(messages.get(1), Some(AgentEvent::Turn(TurnEvent::Ended { .. }))));
90 }
91
92 #[tokio::test]
93 async fn fake_agent_with_tool_call_sends_tool_messages() {
94 let agent = FakeAgent::with_tool_call("bash", "success");
95 let transcript = crate::Transcript::from_stream(agent.run(Task::new("test task"))).await.unwrap();
96 let messages = transcript.events();
97
98 assert_eq!(messages.len(), 3);
99 assert!(matches!(messages.last(), Some(AgentEvent::Turn(TurnEvent::Ended { .. }))));
100 }
101
102 #[tokio::test]
103 async fn fake_agent_writes_file_in_workspace() {
104 let temp_dir = tempfile::tempdir().unwrap();
105 let agent = FakeAgent::writes_file("nested/hello.txt", "hello").with_workspace(temp_dir.path());
106 crate::Transcript::from_stream(agent.run(Task::new("test task"))).await.unwrap();
107 assert_eq!(std::fs::read_to_string(temp_dir.path().join("nested/hello.txt")).unwrap(), "hello");
108 }
109}