Skip to main content

agent_trace/trace/
logs.rs

1use crate::git_store::{CommitInfo, GitStore};
2use crate::types::{Action, Actor, DiffStats, DocType};
3use anyhow::Result;
4use chrono::Utc;
5use std::path::{Path, PathBuf};
6
7/// Generate a human-readable summary for an agent change (no LLM version).
8pub 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
23/// Append a log entry to the session log file, creating it if needed.
24pub 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    // Commit with system authorship.
62    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}