Skip to main content

eval_magic/adapters/
transcript.rs

1//! Harness-neutral transcript types.
2//!
3//! Every harness's transcript parser reduces its native events file to a
4//! [`TranscriptSummary`]; the pipeline consumes only this shape, never a
5//! harness's raw record types.
6
7use serde::{Deserialize, Serialize};
8use std::fs;
9use std::io;
10use std::path::Path;
11
12use crate::core::ToolInvocation;
13
14/// Read a JSONL file, deserializing each non-blank line as `T` and silently
15/// skipping malformed lines (a partial transcript still yields its parseable
16/// records).
17pub(crate) fn read_jsonl<T: serde::de::DeserializeOwned>(path: &Path) -> io::Result<Vec<T>> {
18    let raw = fs::read_to_string(path)?;
19    Ok(raw
20        .split('\n')
21        .filter(|line| !line.trim().is_empty())
22        .filter_map(|line| serde_json::from_str::<T>(line).ok())
23        .collect())
24}
25
26/// A transcript boiled down to the artifacts the pipeline needs.
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct TranscriptSummary {
29    pub tool_invocations: Vec<ToolInvocation>,
30    /// Total token usage (input + output + cache creation/read), as reported by
31    /// the run's terminal `result` event.
32    pub total_tokens: Option<i64>,
33    /// Wall-clock duration, as reported by the run's terminal `result` event.
34    pub duration_ms: Option<i64>,
35    /// Concatenated text blocks of the last assistant message.
36    pub final_text: Option<String>,
37}
38
39#[cfg(test)]
40mod tests {
41    use super::read_jsonl;
42    use serde_json::Value;
43    use std::fs;
44    use tempfile::TempDir;
45
46    #[test]
47    fn read_jsonl_skips_malformed_lines() {
48        let dir = TempDir::new().unwrap();
49        let path = dir.path().join("events.jsonl");
50        fs::write(&path, "{\"a\":1}\nnot json\n{\"a\":2}\n").unwrap();
51        let values: Vec<Value> = read_jsonl(&path).unwrap();
52        assert_eq!(values.len(), 2);
53        assert_eq!(values[0]["a"], 1);
54        assert_eq!(values[1]["a"], 2);
55    }
56
57    #[test]
58    fn read_jsonl_skips_blank_and_whitespace_only_lines() {
59        let dir = TempDir::new().unwrap();
60        let path = dir.path().join("events.jsonl");
61        fs::write(&path, "{\"a\":1}\n\n   \n\t\n{\"a\":2}\n").unwrap();
62        let values: Vec<Value> = read_jsonl(&path).unwrap();
63        assert_eq!(values.len(), 2);
64    }
65
66    #[test]
67    fn read_jsonl_errors_on_a_missing_file() {
68        let dir = TempDir::new().unwrap();
69        let missing = dir.path().join("absent.jsonl");
70        assert!(read_jsonl::<Value>(&missing).is_err());
71    }
72}