agent_trace/trace/
logs.rs1use crate::git_store::{CommitInfo, GitStore};
2use crate::types::{Action, Actor, DiffStats, DocType};
3use anyhow::Result;
4use chrono::Utc;
5use std::path::{Path, PathBuf};
6
7pub fn summarize_change_no_llm(
9 path: &Path,
10 _doc_type: &DocType,
11 stats: &DiffStats,
12 agent_name: &str,
13) -> String {
14 format!(
15 "Agent {} modified {}: +{} lines, -{} lines.",
16 agent_name,
17 path.display(),
18 stats.lines_added,
19 stats.lines_removed,
20 )
21}
22
23pub fn append_agent_log(
25 store_root: &Path,
26 git: &GitStore,
27 agent_name: &str,
28 session_id: &str,
29 entries: &[LogSynthEntry],
30) -> Result<()> {
31 if entries.is_empty() {
32 return Ok(());
33 }
34
35 let logs_dir = store_root.join("logs");
36 std::fs::create_dir_all(&logs_dir)?;
37
38 let log_path = logs_dir.join(format!("{agent_name}-{session_id}.md"));
39 let rel_log_path = log_path
40 .strip_prefix(store_root)
41 .unwrap_or(&log_path)
42 .to_path_buf();
43
44 let mut content = if log_path.exists() {
45 std::fs::read_to_string(&log_path)?
46 } else {
47 format!("# Agent Log: {agent_name} (session {session_id})\n\n")
48 };
49
50 for entry in entries {
51 content.push_str(&format!(
52 "## {} — {}\n\n{}\n\n",
53 entry.timestamp.format("%Y-%m-%d %H:%M:%S UTC"),
54 entry.path.display(),
55 entry.summary,
56 ));
57 }
58
59 std::fs::write(&log_path, &content)?;
60
61 let info = CommitInfo {
63 action: Action::Modify,
64 files: vec![(rel_log_path, Action::Modify, DocType::Log)],
65 actor: Actor::System,
66 summary: format!("update agent log for {agent_name}"),
67 agent_name: Some(agent_name.to_string()),
68 session_id: Some(session_id.to_string()),
69 };
70 git.commit(&info)?;
71
72 Ok(())
73}
74
75pub struct LogSynthEntry {
76 pub timestamp: chrono::DateTime<Utc>,
77 pub path: PathBuf,
78 pub summary: String,
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use crate::git_store::GitStore;
85 use crate::types::DiffStats;
86 use tempfile::TempDir;
87
88 #[test]
89 fn test_summarize_change_no_llm() {
90 let stats = DiffStats {
91 lines_added: 15,
92 lines_removed: 3,
93 };
94 let summary = summarize_change_no_llm(
95 &PathBuf::from("impl-plan.md"),
96 &DocType::Plan,
97 &stats,
98 "claude-code",
99 );
100 assert!(summary.contains("claude-code"));
101 assert!(summary.contains("+15"));
102 assert!(summary.contains("-3"));
103 assert!(summary.contains("impl-plan.md"));
104 }
105
106 #[test]
107 fn test_append_agent_log_creates_file() {
108 let tmp = TempDir::new().unwrap();
109 let root = tmp.path();
110 std::fs::create_dir_all(root.join(".agent-trace")).unwrap();
111 let git = GitStore::init(root).unwrap();
112
113 let entries = vec![LogSynthEntry {
114 timestamp: Utc::now(),
115 path: PathBuf::from("prd.md"),
116 summary: "Agent modified prd.md: +5 lines, -1 lines.".into(),
117 }];
118
119 append_agent_log(root, &git, "claude-code", "ses001", &entries).unwrap();
120
121 let log_file = root.join("logs").join("claude-code-ses001.md");
122 assert!(log_file.exists());
123 let content = std::fs::read_to_string(&log_file).unwrap();
124 assert!(content.contains("Agent Log: claude-code"));
125 assert!(content.contains("prd.md"));
126 }
127
128 #[test]
129 fn test_append_agent_log_appends_to_existing() {
130 let tmp = TempDir::new().unwrap();
131 let root = tmp.path();
132 std::fs::create_dir_all(root.join(".agent-trace")).unwrap();
133 let git = GitStore::init(root).unwrap();
134
135 let entries1 = vec![LogSynthEntry {
136 timestamp: Utc::now(),
137 path: PathBuf::from("a.md"),
138 summary: "first".into(),
139 }];
140 append_agent_log(root, &git, "aider", "ses1", &entries1).unwrap();
141
142 let entries2 = vec![LogSynthEntry {
143 timestamp: Utc::now(),
144 path: PathBuf::from("b.md"),
145 summary: "second".into(),
146 }];
147 append_agent_log(root, &git, "aider", "ses1", &entries2).unwrap();
148
149 let content = std::fs::read_to_string(root.join("logs").join("aider-ses1.md")).unwrap();
150 assert!(content.contains("first"));
151 assert!(content.contains("second"));
152 }
153}