vtcode_core/core/
trajectory.rs

1use serde::Serialize;
2use std::fs::{OpenOptions, create_dir_all};
3use std::io::Write;
4use std::path::{Path, PathBuf};
5
6#[derive(Clone)]
7pub struct TrajectoryLogger {
8    path: PathBuf,
9    enabled: bool,
10}
11
12impl TrajectoryLogger {
13    pub fn new(workspace: &Path) -> Self {
14        let dir = workspace.join("logs");
15        let _ = create_dir_all(&dir);
16        let path = dir.join("trajectory.jsonl");
17        Self {
18            path,
19            enabled: true,
20        }
21    }
22
23    pub fn disabled() -> Self {
24        Self {
25            path: PathBuf::from("/dev/null"),
26            enabled: false,
27        }
28    }
29
30    pub fn log<T: Serialize>(&self, record: &T) {
31        if !self.enabled {
32            return;
33        }
34        if let Ok(line) = serde_json::to_string(record) {
35            if let Ok(mut f) = OpenOptions::new()
36                .create(true)
37                .append(true)
38                .open(&self.path)
39            {
40                let _ = writeln!(f, "{}", line);
41            }
42        }
43    }
44
45    pub fn log_route(&self, turn: usize, selected_model: &str, class: &str, input_preview: &str) {
46        #[derive(Serialize)]
47        struct RouteRec<'a> {
48            kind: &'static str,
49            turn: usize,
50            selected_model: &'a str,
51            class: &'a str,
52            input_preview: &'a str,
53            ts: i64,
54        }
55        let rec = RouteRec {
56            kind: "route",
57            turn,
58            selected_model,
59            class,
60            input_preview,
61            ts: chrono::Utc::now().timestamp(),
62        };
63        self.log(&rec);
64    }
65
66    pub fn log_tool_call(&self, turn: usize, name: &str, args: &serde_json::Value, ok: bool) {
67        #[derive(Serialize)]
68        struct ToolRec<'a> {
69            kind: &'static str,
70            turn: usize,
71            name: &'a str,
72            args: serde_json::Value,
73            ok: bool,
74            ts: i64,
75        }
76        let rec = ToolRec {
77            kind: "tool",
78            turn,
79            name,
80            args: args.clone(),
81            ok,
82            ts: chrono::Utc::now().timestamp(),
83        };
84        self.log(&rec);
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use std::fs;
92    use tempfile::TempDir;
93
94    #[test]
95    fn test_trajectory_logger_log_route_integration() {
96        let temp_dir = TempDir::new().unwrap();
97        let logger = TrajectoryLogger::new(temp_dir.path());
98
99        // Test the logging functionality that would be called in the agent loop
100        logger.log_route(
101            1,
102            "gemini-2.5-flash",
103            "standard",
104            "test user input for logging",
105        );
106
107        // Check that the log file was created and contains expected content
108        let log_path = temp_dir.path().join("logs/trajectory.jsonl");
109        assert!(log_path.exists());
110
111        let content = fs::read_to_string(log_path).unwrap();
112        let lines: Vec<&str> = content.lines().collect();
113        assert_eq!(lines.len(), 1);
114
115        // Parse the JSON and verify content
116        let record: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
117        assert_eq!(record["kind"], "route");
118        assert_eq!(record["turn"], 1);
119        assert_eq!(record["selected_model"], "gemini-2.5-flash");
120        assert_eq!(record["class"], "standard");
121        assert_eq!(record["input_preview"], "test user input for logging");
122        assert!(record["ts"].is_number());
123    }
124}