vtcode_core/core/
trajectory.rs1use 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 logger.log_route(
101 1,
102 "gemini-2.5-flash",
103 "standard",
104 "test user input for logging",
105 );
106
107 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 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}