Skip to main content

opensession_core/
agent_metrics.rs

1use crate::trace::{Event, EventType, Session};
2use std::collections::HashSet;
3
4fn normalize_task_id(event: &Event) -> Option<&str> {
5    event
6        .task_id
7        .as_deref()
8        .map(str::trim)
9        .filter(|task_id| !task_id.is_empty())
10}
11
12/// Max number of concurrently active agents (main lane included).
13///
14/// Always returns `>= 1` (main lane baseline).
15pub fn max_active_agents(session: &Session) -> usize {
16    if session.events.is_empty() {
17        return 1;
18    }
19
20    let mut active_task_ids: HashSet<&str> = HashSet::new();
21    let mut max_subagents = 0usize;
22
23    for event in &session.events {
24        let task_id = normalize_task_id(event);
25
26        if matches!(event.event_type, EventType::TaskStart { .. }) {
27            if let Some(task_id) = task_id {
28                active_task_ids.insert(task_id);
29            }
30        }
31
32        max_subagents = max_subagents.max(active_task_ids.len());
33
34        if matches!(event.event_type, EventType::TaskEnd { .. }) {
35            if let Some(task_id) = task_id {
36                active_task_ids.remove(task_id);
37            }
38        }
39    }
40
41    max_subagents + 1
42}
43
44#[cfg(test)]
45mod tests {
46    use super::max_active_agents;
47    use crate::trace::{Agent, Content, Event, EventType, Session};
48    use chrono::Utc;
49    use std::collections::HashMap;
50
51    fn event(id: &str, event_type: EventType, task_id: Option<&str>) -> Event {
52        Event {
53            event_id: id.to_string(),
54            timestamp: Utc::now(),
55            event_type,
56            task_id: task_id.map(ToString::to_string),
57            content: Content::empty(),
58            duration_ms: None,
59            attributes: HashMap::new(),
60        }
61    }
62
63    fn session(tool: &str, events: Vec<Event>) -> Session {
64        Session {
65            version: "hail-1.0.0".to_string(),
66            session_id: "s1".to_string(),
67            agent: Agent {
68                provider: "p".to_string(),
69                model: "m".to_string(),
70                tool: tool.to_string(),
71                tool_version: None,
72            },
73            context: crate::trace::SessionContext {
74                title: None,
75                description: None,
76                tags: vec![],
77                created_at: Utc::now(),
78                updated_at: Utc::now(),
79                related_session_ids: vec![],
80                attributes: HashMap::new(),
81            },
82            events,
83            stats: Default::default(),
84        }
85    }
86
87    #[test]
88    fn returns_one_for_empty_sessions() {
89        let s = session("codex", vec![]);
90        assert_eq!(max_active_agents(&s), 1);
91    }
92
93    #[test]
94    fn counts_concurrent_tasks() {
95        let s = session(
96            "codex",
97            vec![
98                event("1", EventType::TaskStart { title: None }, Some("t1")),
99                event("2", EventType::TaskStart { title: None }, Some("t2")),
100                event("3", EventType::TaskEnd { summary: None }, Some("t2")),
101                event("4", EventType::TaskEnd { summary: None }, Some("t1")),
102            ],
103        );
104        assert_eq!(max_active_agents(&s), 3);
105    }
106
107    #[test]
108    fn counts_merged_claude_subagents_for_agent_concurrency() {
109        let sub = event("1", EventType::TaskStart { title: None }, Some("sub-1"));
110        let s = session(
111            "claude-code",
112            vec![
113                sub,
114                event("2", EventType::TaskEnd { summary: None }, Some("sub-1")),
115            ],
116        );
117        assert_eq!(max_active_agents(&s), 2);
118    }
119}