agent-trace 0.1.0

Git-backed document memory, trace continuity, and permissioned writes for agent workflows
Documentation
use crate::llm::trace_insights::TraceDocument;
use crate::types::DocType;

const MAX_SNIPPET_CHARS: usize = 2000;

#[derive(Debug, Clone)]
pub struct DocSummary {
    pub path: String,
    pub doc_type: DocType,
    pub content_snippet: String,
}

fn truncate(s: &str, max: usize) -> &str {
    if s.len() <= max {
        s
    } else {
        &s[..max]
    }
}

pub fn summarize_change_prompt(path: &str, doc_type: &str, diff: &str) -> String {
    format!(
        "Summarize this diff of a {doc_type} document '{path}' in one sentence (max 20 words).\n\n{}",
        truncate(diff, MAX_SNIPPET_CHARS)
    )
}

pub fn synthesize_context_prompt(documents: &[DocSummary], updates: &[String]) -> String {
    let mut docs_str = String::new();
    for doc in documents {
        docs_str.push_str(&format!(
            "[{}] {}: {}\n",
            doc.doc_type, doc.path, doc.content_snippet
        ));
    }
    format!(
        "Given these project documents and user updates, produce a concise context document \
         covering: goals, status, key decisions, architecture, open questions.\n\
         Start with '# Project Context'.\n\n\
         Documents:\n{}\n\nUser updates:\n{}",
        truncate(&docs_str, MAX_SNIPPET_CHARS),
        truncate(&updates.join("\n"), MAX_SNIPPET_CHARS / 2)
    )
}

pub fn summarize_session_prompt(session_id: &str, events: &[String]) -> String {
    format!(
        "Summarize this agent session in 2-3 sentences for a reconnecting agent.\n\
         Session: {session_id}\n\nEvents:\n{}",
        truncate(&events.join("\n"), MAX_SNIPPET_CHARS)
    )
}

pub fn summarize_event_history_prompt(events: &str) -> String {
    format!(
        "Summarize this agent work history in one paragraph (4-6 sentences).\n\
         Focus on themes and progress, not per-file enumeration.\n\n{}",
        truncate(events, MAX_SNIPPET_CHARS)
    )
}

pub fn update_running_summary_prompt(previous: &str, events: &str, plan: &str) -> String {
    format!(
        "Update the running project summary given the previous version and new events.\n\
         Keep sections: Current Status, Recent Activity, Resume Here, Key Documents, Open Items.\n\
         Under 800 words.\n\n\
         Previous summary:\n{}\n\nNew events:\n{}\n\nPlan excerpt:\n{}",
        truncate(previous, 3000),
        truncate(events, 2000),
        truncate(plan, 2000)
    )
}

pub fn trace_to_doc_summaries(documents: &[TraceDocument]) -> Vec<DocSummary> {
    documents
        .iter()
        .map(|d| DocSummary {
            path: d.path.clone(),
            doc_type: d.doc_type.clone(),
            content_snippet: d.content_snippet.clone(),
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn prompts_are_non_empty() {
        assert!(!summarize_change_prompt("a.md", "plan", "+1").is_empty());
        assert!(!update_running_summary_prompt("prev", "ev", "plan").is_empty());
        assert!(!summarize_session_prompt("s1", &["event".into()]).is_empty());
    }
}