1use indexmap::IndexMap;
2use jiff::Timestamp;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fmt::{self, Display};
6use std::str::FromStr;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct SessionId(String);
11
12impl SessionId {
13 #[must_use]
14 #[allow(dead_code)]
15 pub fn new(id: String) -> Self {
16 Self(id)
17 }
18
19 #[must_use]
20 #[allow(dead_code)]
21 pub fn as_str(&self) -> &str {
22 &self.0
23 }
24}
25
26impl Display for SessionId {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 write!(f, "{}", self.0)
29 }
30}
31
32impl From<String> for SessionId {
33 fn from(id: String) -> Self {
34 Self(id)
35 }
36}
37
38impl From<&str> for SessionId {
39 fn from(id: &str) -> Self {
40 Self(id.to_string())
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
45pub struct AgentType(String);
46
47impl AgentType {
48 #[must_use]
49 #[allow(dead_code)]
50 pub fn new(agent_type: String) -> Self {
51 Self(agent_type)
52 }
53
54 #[must_use]
55 #[allow(dead_code)]
56 pub fn as_str(&self) -> &str {
57 &self.0
58 }
59}
60
61impl Display for AgentType {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 write!(f, "{}", self.0)
64 }
65}
66
67impl From<String> for AgentType {
68 fn from(agent_type: String) -> Self {
69 Self(agent_type)
70 }
71}
72
73impl From<&str> for AgentType {
74 fn from(agent_type: &str) -> Self {
75 Self(agent_type.to_string())
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
80pub struct MessageId(String);
81
82impl MessageId {
83 #[must_use]
84 #[allow(dead_code)]
85 pub fn new(id: String) -> Self {
86 Self(id)
87 }
88
89 #[must_use]
90 #[allow(dead_code)]
91 pub fn as_str(&self) -> &str {
92 &self.0
93 }
94}
95
96impl Display for MessageId {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 write!(f, "{}", self.0)
99 }
100}
101
102impl From<String> for MessageId {
103 fn from(id: String) -> Self {
104 Self(id)
105 }
106}
107
108impl From<&str> for MessageId {
109 fn from(id: &str) -> Self {
110 Self(id.to_string())
111 }
112}
113
114impl AsRef<str> for SessionId {
115 fn as_ref(&self) -> &str {
116 &self.0
117 }
118}
119
120impl AsRef<str> for AgentType {
121 fn as_ref(&self) -> &str {
122 &self.0
123 }
124}
125
126impl AsRef<str> for MessageId {
127 fn as_ref(&self) -> &str {
128 &self.0
129 }
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct SessionEntry {
136 pub uuid: String,
137 pub parent_uuid: Option<String>,
138 pub session_id: String,
139 pub timestamp: String,
140 pub user_type: String,
141 pub message: Message,
142 #[serde(rename = "type")]
143 pub entry_type: String,
144 pub cwd: Option<String>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148#[serde(untagged)]
149pub enum Message {
150 User {
151 role: String,
152 content: String,
153 },
154 Assistant {
155 role: String,
156 content: Vec<ContentBlock>,
157 #[serde(default)]
158 id: Option<String>,
159 #[serde(default)]
160 model: Option<String>,
161 },
162 ToolResult {
163 role: String,
164 content: Vec<ToolResultContent>,
165 },
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(tag = "type", rename_all = "snake_case")]
170pub enum ContentBlock {
171 Text {
172 text: String,
173 },
174 ToolUse {
175 id: String,
176 name: String,
177 input: serde_json::Value,
178 },
179 Thinking {
180 thinking: String,
181 },
182 ServerToolUse {
183 id: String,
184 name: String,
185 input: serde_json::Value,
186 },
187 AdvisorToolResult {
188 tool_use_id: String,
189 content: serde_json::Value,
190 },
191 #[serde(other)]
192 Unknown,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct ToolResultContent {
197 pub tool_use_id: String,
198 #[serde(rename = "type")]
199 pub content_type: String,
200 pub content: String,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct AgentInvocation {
206 pub timestamp: Timestamp,
207 pub agent_type: String,
208 pub task_description: String,
209 pub prompt: String,
210 pub files_modified: Vec<String>,
211 pub tools_used: Vec<String>,
212 pub duration_ms: Option<u64>,
213 pub parent_message_id: String,
214 pub session_id: String,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct FileOperation {
220 pub timestamp: Timestamp,
221 pub operation: FileOpType,
222 pub file_path: String,
223 pub agent_context: Option<String>,
224 pub session_id: String,
225 pub message_id: String,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct ToolInvocation {
231 pub timestamp: Timestamp,
232 pub tool_name: String,
233 pub tool_category: ToolCategory,
234 pub command_line: String,
235 pub arguments: Vec<String>,
236 pub flags: HashMap<String, String>,
237 pub exit_code: Option<i32>,
238 pub agent_context: Option<String>,
239 pub session_id: String,
240 pub message_id: String,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
245pub enum ToolCategory {
246 PackageManager,
247 BuildTool,
248 Testing,
249 Linting,
250 Git,
251 CloudDeploy,
252 Database,
253 Other(String),
254}
255
256impl ToolCategory {
257 #[must_use]
260 #[allow(dead_code)]
261 pub fn from_string(s: &str) -> Self {
262 match s {
263 "PackageManager" => ToolCategory::PackageManager,
264 "BuildTool" => ToolCategory::BuildTool,
265 "Testing" => ToolCategory::Testing,
266 "Linting" => ToolCategory::Linting,
267 "Git" => ToolCategory::Git,
268 "CloudDeploy" => ToolCategory::CloudDeploy,
269 "Database" => ToolCategory::Database,
270 _ => ToolCategory::Other(s.to_string()),
271 }
272 }
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct ToolStatistics {
278 pub tool_name: String,
279 pub category: ToolCategory,
280 pub total_invocations: u32,
281 pub agents_using: Vec<String>,
282 pub success_count: u32,
283 pub failure_count: u32,
284 pub first_seen: Timestamp,
285 pub last_seen: Timestamp,
286 pub command_patterns: Vec<String>,
287 pub sessions: Vec<String>,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub enum FileOpType {
292 Read,
293 Write,
294 Edit,
295 MultiEdit,
296 Delete,
297 Glob,
298 Grep,
299}
300
301impl FromStr for FileOpType {
302 type Err = anyhow::Error;
303
304 fn from_str(s: &str) -> Result<Self, Self::Err> {
305 match s {
306 "Read" => Ok(FileOpType::Read),
307 "Write" => Ok(FileOpType::Write),
308 "Edit" => Ok(FileOpType::Edit),
309 "MultiEdit" => Ok(FileOpType::MultiEdit),
310 "Delete" => Ok(FileOpType::Delete),
311 "Glob" => Ok(FileOpType::Glob),
312 "Grep" => Ok(FileOpType::Grep),
313 _ => Err(anyhow::anyhow!("Unknown file operation type: {s}")),
314 }
315 }
316}
317
318#[derive(Debug, Serialize, Deserialize)]
320pub struct SessionAnalysis {
321 pub session_id: String,
322 pub project_path: String,
323 pub start_time: Timestamp,
324 pub end_time: Timestamp,
325 pub duration_ms: u64,
326 pub agents: Vec<AgentInvocation>,
327 pub file_operations: Vec<FileOperation>,
328 pub file_to_agents: IndexMap<String, Vec<AgentAttribution>>,
329 pub agent_stats: IndexMap<String, AgentStatistics>,
330 pub collaboration_patterns: Vec<CollaborationPattern>,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct AgentAttribution {
336 pub agent_type: String,
337 pub contribution_percent: f32,
338 pub confidence_score: f32,
339 pub operations: Vec<String>,
340 pub first_interaction: Timestamp,
341 pub last_interaction: Timestamp,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct AgentStatistics {
347 pub agent_type: String,
348 pub total_invocations: u32,
349 pub total_duration_ms: u64,
350 pub files_touched: u32,
351 pub tools_used: Vec<String>,
352 pub first_seen: Timestamp,
353 pub last_seen: Timestamp,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct CollaborationPattern {
359 pub pattern_type: String,
360 pub agents: Vec<String>,
361 pub description: String,
362 pub frequency: u32,
363 pub confidence: f32,
364}
365
366#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct AgentToolCorrelation {
369 pub agent_type: String,
370 pub tool_name: String,
371 pub usage_count: u32,
372 pub success_rate: f32,
373 pub average_invocations_per_session: f32,
374}
375
376#[derive(Debug, Serialize, Deserialize)]
378pub struct ToolAnalysis {
379 pub session_id: String,
380 pub total_tool_invocations: u32,
381 pub tool_statistics: IndexMap<String, ToolStatistics>,
382 pub agent_tool_correlations: Vec<AgentToolCorrelation>,
383 pub tool_chains: Vec<ToolChain>,
384 pub category_breakdown: IndexMap<ToolCategory, u32>,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct ToolChain {
390 pub tools: Vec<String>,
391 pub frequency: u32,
392 pub average_time_between_ms: u64,
393 pub typical_agent: Option<String>,
394 pub success_rate: f32,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct AnalyzerConfig {
400 pub session_dirs: Vec<String>,
401 pub agent_confidence_threshold: f32,
402 pub file_attribution_window_ms: u64,
403 pub exclude_patterns: Vec<String>,
404}
405
406impl Default for AnalyzerConfig {
407 fn default() -> Self {
408 Self {
409 session_dirs: vec![],
410 agent_confidence_threshold: 0.7,
411 file_attribution_window_ms: 300_000, exclude_patterns: vec![
413 "node_modules/".to_string(),
414 "target/".to_string(),
415 ".git/".to_string(),
416 ],
417 }
418 }
419}
420
421pub fn parse_timestamp(timestamp_str: &str) -> Result<Timestamp, anyhow::Error> {
427 Timestamp::from_str(timestamp_str)
429 .map_err(|e| anyhow::anyhow!("Failed to parse timestamp '{timestamp_str}': {e}"))
430}
431
432#[must_use]
434pub fn extract_file_path(input: &serde_json::Value) -> Option<String> {
435 for field in &["file_path", "path", "pattern"] {
437 if let Some(path) = input.get(field).and_then(|v| v.as_str()) {
438 return Some(path.to_string());
439 }
440 }
441
442 if let Some(edits) = input.get("edits").and_then(|v| v.as_array()) {
444 if !edits.is_empty() {
445 if let Some(file_path) = input.get("file_path").and_then(|v| v.as_str()) {
446 return Some(file_path.to_string());
447 }
448 }
449 }
450
451 None
452}
453
454#[allow(dead_code)]
457#[must_use]
458pub fn normalize_agent_name(agent_type: &str) -> String {
459 agent_type.to_lowercase().replace(['-', ' '], "_")
460}
461
462#[allow(dead_code)]
464#[must_use]
465pub fn get_agent_category(agent_type: &str) -> &'static str {
466 match agent_type {
467 "architect" | "backend-architect" | "frontend-developer" => "architecture",
468 "developer" | "rapid-prototyper" => "development",
469 "rust-performance-expert" | "rust-code-reviewer" => "rust-expert",
470 "debugger" | "test-writer-fixer" => "testing",
471 "technical-writer" => "documentation",
472 "devops-automator" | "overseer" => "operations",
473 "general-purpose" => "general",
474 _ => "other",
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn test_parse_timestamp() {
484 let timestamp_str = "2025-10-01T09:05:21.902Z";
485 let result = parse_timestamp(timestamp_str);
486 assert!(result.is_ok());
487 }
488
489 #[test]
490 fn test_newtype_wrappers() {
491 let session_id = SessionId::new("test-session".to_string());
493 assert_eq!(session_id.as_str(), "test-session");
494 assert_eq!(session_id.to_string(), "test-session");
495 assert_eq!(session_id.as_ref(), "test-session");
496
497 let session_id_from_str: SessionId = "another-session".into();
498 assert_eq!(session_id_from_str.as_str(), "another-session");
499
500 let agent_type = AgentType::new("architect".to_string());
502 assert_eq!(agent_type.as_str(), "architect");
503 assert_eq!(agent_type.to_string(), "architect");
504
505 let message_id = MessageId::new("msg-123".to_string());
507 assert_eq!(message_id.as_str(), "msg-123");
508 assert_eq!(message_id.to_string(), "msg-123");
509 }
510
511 #[test]
512 fn test_extract_file_path() {
513 let input = serde_json::json!({
514 "file_path": "/path/to/file.rs",
515 "description": "Edit file"
516 });
517
518 let path = extract_file_path(&input);
519 assert_eq!(path, Some("/path/to/file.rs".to_string()));
520 }
521
522 #[test]
523 fn test_normalize_agent_name() {
524 assert_eq!(
525 normalize_agent_name("rust-performance-expert"),
526 "rust_performance_expert"
527 );
528 assert_eq!(
529 normalize_agent_name("backend-architect"),
530 "backend_architect"
531 );
532 }
533
534 mod proptest_tests {
535 use super::*;
536 use proptest::prelude::*;
537
538 proptest! {
539 #[test]
540 fn test_normalize_agent_name_properties(
541 input in "[a-zA-Z0-9 -]{1,50}"
542 ) {
543 let result = normalize_agent_name(&input);
544
545 prop_assert!(result.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'));
547
548 if !input.trim().is_empty() {
550 prop_assert!(!result.is_empty());
551 }
552 }
553
554 #[test]
555 fn test_parse_timestamp_properties(
556 year in 2020u16..2030,
557 month in 1u8..=12,
558 day in 1u8..=28, hour in 0u8..=23,
560 minute in 0u8..=59,
561 second in 0u8..=59,
562 millis in 0u16..1000
563 ) {
564 let timestamp_str = format!(
565 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
566 year, month, day, hour, minute, second, millis
567 );
568
569 let result = parse_timestamp(×tamp_str);
570
571 prop_assert!(result.is_ok(), "Failed to parse valid timestamp: {}", timestamp_str);
573
574 if let Ok(parsed) = result {
575 let reformatted = parsed.to_string();
577 prop_assert!(reformatted.starts_with(&year.to_string()));
578 }
579 }
580
581 #[test]
582 fn test_extract_file_path_properties(
583 file_path in r"[a-zA-Z0-9_./\-]{1,100}"
584 ) {
585 let input = serde_json::json!({
586 "file_path": file_path
587 });
588
589 let result = extract_file_path(&input);
590
591 prop_assert_eq!(result, Some(file_path.clone()));
593
594 let input_path = serde_json::json!({
596 "path": file_path
597 });
598 let result_path = extract_file_path(&input_path);
599 prop_assert_eq!(result_path, Some(file_path.clone()));
600 }
601
602 #[test]
603 fn test_newtype_wrapper_roundtrip(
604 session_id in "[a-zA-Z0-9-]{10,50}",
605 agent_type in "[a-zA-Z0-9-_]{3,30}",
606 message_id in "[a-zA-Z0-9-]{10,50}"
607 ) {
608 let session = SessionId::new(session_id.clone());
610 prop_assert_eq!(session.as_str(), &session_id);
611 prop_assert_eq!(session.to_string(), session_id);
612
613 let agent = AgentType::new(agent_type.clone());
615 prop_assert_eq!(agent.as_str(), &agent_type);
616 prop_assert_eq!(agent.to_string(), agent_type);
617
618 let message = MessageId::new(message_id.clone());
620 prop_assert_eq!(message.as_str(), &message_id);
621 prop_assert_eq!(message.to_string(), message_id);
622 }
623 }
624 }
625}