opensession_core/
agent_metrics.rs1use 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
12pub 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}