Skip to main content

ai_agent/services/
agent_summary.rs

1//! Agent summary service - periodic background summarization for coordinator mode sub-agents.
2//!
3//! Translates AgentSummary/agentSummary.ts from claude code.
4
5pub const SUMMARY_INTERVAL_MS: u64 = 30000;
6
7pub fn build_summary_prompt(previous_summary: Option<&str>) -> String {
8    let prev_line = previous_summary
9        .map(|s| format!("\nPrevious: \"{}\" — say something NEW.\n", s))
10        .unwrap_or_default();
11
12    format!(
13        r#"Describe your most recent action in 3-5 words using present tense (-ing). Name the file or function, not the branch. Do not use tools.
14{}
15Good: "Reading runAgent.ts"
16Good: "Fixing null check in validate.ts"
17Good: "Running auth module tests"
18Good: "Adding retry logic to fetchUser"
19
20Bad (past tense): "Analyzed the branch diff"
21Bad (too vague): "Investigating the issue"
22Bad (too long): "Reviewing full branch diff and AgentTool.tsx integration"
23Bad (branch name): "Analyzed adam/background-summary branch diff""#,
24        prev_line
25    )
26}
27
28/// Handle for stopping agent summarization
29pub struct AgentSummaryHandle {
30    pub stop: Box<dyn Fn() + Send + Sync>,
31}
32
33impl AgentSummaryHandle {
34    pub fn new(stop: impl Fn() + Send + Sync + 'static) -> Self {
35        Self {
36            stop: Box::new(stop),
37        }
38    }
39}
40
41impl Clone for AgentSummaryHandle {
42    fn clone(&self) -> Self {
43        // Note: This is a workaround - the actual stop function cannot be cloned
44        // In practice, you would need to use Arc<dyn Fn()> for true cloneability
45        Self {
46            stop: Box::new(|| {}),
47        }
48    }
49}
50
51/// State for agent summarization
52pub struct AgentSummaryState {
53    pub task_id: String,
54    pub agent_id: String,
55    pub summary: Option<String>,
56    pub is_running: bool,
57}
58
59impl AgentSummaryState {
60    pub fn new(task_id: impl Into<String>, agent_id: impl Into<String>) -> Self {
61        Self {
62            task_id: task_id.into(),
63            agent_id: agent_id.into(),
64            summary: None,
65            is_running: true,
66        }
67    }
68
69    pub fn stop(&mut self) {
70        self.is_running = false;
71    }
72
73    pub fn update_summary(&mut self, summary: impl Into<String>) {
74        self.summary = Some(summary.into());
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn test_build_summary_prompt_no_previous() {
84        let prompt = build_summary_prompt(None);
85        assert!(prompt.contains("Describe your most recent action"));
86        assert!(!prompt.contains("Previous:"));
87    }
88
89    #[test]
90    fn test_build_summary_prompt_with_previous() {
91        let prompt = build_summary_prompt(Some("Reading file"));
92        assert!(prompt.contains("Previous:"));
93        assert!(prompt.contains("Reading file"));
94    }
95
96    #[test]
97    fn test_agent_summary_state() {
98        let mut state = AgentSummaryState::new("task-1", "agent-1");
99
100        assert!(state.summary.is_none());
101        assert!(state.is_running);
102
103        state.update_summary("Reading file");
104        assert_eq!(state.summary, Some("Reading file".to_string()));
105
106        state.stop();
107        assert!(!state.is_running);
108    }
109
110    #[test]
111    fn test_summary_interval() {
112        assert_eq!(SUMMARY_INTERVAL_MS, 30000);
113    }
114}