aether_evals/evals/
transcript.rs1use crate::agents::{TRANSCRIPT_PAYLOAD_CHARS, get_transcript_line};
2use aether_core::events::AgentMessage;
3use std::fmt::Write as _;
4
5pub struct Transcript {
6 messages: Vec<AgentMessage>,
7}
8
9pub struct ToolCall<'a> {
10 pub name: &'a str,
11 pub arguments: &'a str,
12}
13
14impl Transcript {
15 pub(crate) fn new(messages: Vec<AgentMessage>) -> Self {
16 Self { messages }
17 }
18
19 pub fn messages(&self) -> &[AgentMessage] {
20 &self.messages
21 }
22
23 pub fn all_tool_calls(&self) -> impl Iterator<Item = ToolCall<'_>> + '_ {
24 self.messages.iter().filter_map(|message| match message {
25 AgentMessage::ToolResult { result, .. } => {
26 Some(ToolCall { name: &result.name, arguments: &result.arguments })
27 }
28 AgentMessage::ToolError { error, .. } => {
29 Some(ToolCall { name: &error.name, arguments: error.arguments.as_deref().unwrap_or("") })
30 }
31 _ => None,
32 })
33 }
34
35 pub fn tool_calls<'a>(&'a self, name: &'a str) -> impl Iterator<Item = ToolCall<'a>> + 'a {
36 self.all_tool_calls().filter(move |call| call.name == name)
37 }
38
39 pub fn tool_called(&self, name: &str) -> bool {
40 self.tool_calls(name).next().is_some()
41 }
42
43 pub fn tool_call_count(&self, name: &str) -> usize {
44 self.tool_calls(name).count()
45 }
46}
47
48impl ToolCall<'_> {
49 pub fn arguments_json(&self) -> Result<serde_json::Value, serde_json::Error> {
50 serde_json::from_str(self.arguments)
51 }
52}
53
54pub(crate) fn format_transcript(messages: &[AgentMessage]) -> String {
55 let mut transcript = String::new();
56 for message in messages {
57 if let Some(line) = get_transcript_line(message, TRANSCRIPT_PAYLOAD_CHARS) {
58 let _ = writeln!(transcript, "{line}");
59 }
60 }
61 transcript
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use llm::{ToolCallRequest, ToolCallResult};
68
69 #[test]
70 fn tool_call_count_counts_matching_tool_calls() {
71 let transcript = transcript_with_messages(vec![tool_call("bash"), tool_call("read"), tool_result("bash")]);
72
73 assert!(transcript.tool_called("bash"));
74 assert!(!transcript.tool_called("read"));
75 assert!(!transcript.tool_called("write"));
76 assert_eq!(transcript.tool_call_count("bash"), 1);
77 assert_eq!(transcript.tool_call_count("read"), 0);
78 }
79
80 #[test]
81 fn tool_call_arguments_json_parses_arguments() {
82 let call = ToolCall { name: "bash", arguments: r#"{"command":"pwd"}"# };
83
84 assert_eq!(call.arguments_json().unwrap(), serde_json::json!({ "command": "pwd" }));
85 }
86
87 #[test]
88 fn tool_call_arguments_json_returns_error_for_invalid_json() {
89 let call = ToolCall { name: "bash", arguments: "not json" };
90
91 assert!(call.arguments_json().is_err());
92 }
93
94 pub(crate) fn transcript_with_messages(messages: Vec<AgentMessage>) -> Transcript {
95 Transcript::new(messages)
96 }
97
98 fn tool_call(name: &str) -> AgentMessage {
99 AgentMessage::ToolCall {
100 request: ToolCallRequest { id: name.to_string(), name: name.to_string(), arguments: "{}".to_string() },
101 model_name: "test".to_string(),
102 }
103 }
104
105 fn tool_result(name: &str) -> AgentMessage {
106 AgentMessage::ToolResult {
107 result: ToolCallResult {
108 id: name.to_string(),
109 name: name.to_string(),
110 arguments: "{}".to_string(),
111 result: "ok".to_string(),
112 },
113 result_meta: None,
114 model_name: "test".to_string(),
115 }
116 }
117}