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}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct ToolResultContent {
183 pub tool_use_id: String,
184 #[serde(rename = "type")]
185 pub content_type: String,
186 pub content: String,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct AgentInvocation {
192 pub timestamp: Timestamp,
193 pub agent_type: String,
194 pub task_description: String,
195 pub prompt: String,
196 pub files_modified: Vec<String>,
197 pub tools_used: Vec<String>,
198 pub duration_ms: Option<u64>,
199 pub parent_message_id: String,
200 pub session_id: String,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct FileOperation {
206 pub timestamp: Timestamp,
207 pub operation: FileOpType,
208 pub file_path: String,
209 pub agent_context: Option<String>,
210 pub session_id: String,
211 pub message_id: String,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct ToolInvocation {
217 pub timestamp: Timestamp,
218 pub tool_name: String,
219 pub tool_category: ToolCategory,
220 pub command_line: String,
221 pub arguments: Vec<String>,
222 pub flags: HashMap<String, String>,
223 pub exit_code: Option<i32>,
224 pub agent_context: Option<String>,
225 pub session_id: String,
226 pub message_id: String,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
231pub enum ToolCategory {
232 PackageManager,
233 BuildTool,
234 Testing,
235 Linting,
236 Git,
237 CloudDeploy,
238 Database,
239 Other(String),
240}
241
242impl ToolCategory {
243 #[must_use]
246 #[allow(dead_code)]
247 pub fn from_string(s: &str) -> Self {
248 match s {
249 "PackageManager" => ToolCategory::PackageManager,
250 "BuildTool" => ToolCategory::BuildTool,
251 "Testing" => ToolCategory::Testing,
252 "Linting" => ToolCategory::Linting,
253 "Git" => ToolCategory::Git,
254 "CloudDeploy" => ToolCategory::CloudDeploy,
255 "Database" => ToolCategory::Database,
256 _ => ToolCategory::Other(s.to_string()),
257 }
258 }
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct ToolStatistics {
264 pub tool_name: String,
265 pub category: ToolCategory,
266 pub total_invocations: u32,
267 pub agents_using: Vec<String>,
268 pub success_count: u32,
269 pub failure_count: u32,
270 pub first_seen: Timestamp,
271 pub last_seen: Timestamp,
272 pub command_patterns: Vec<String>,
273 pub sessions: Vec<String>,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub enum FileOpType {
278 Read,
279 Write,
280 Edit,
281 MultiEdit,
282 Delete,
283 Glob,
284 Grep,
285}
286
287impl FromStr for FileOpType {
288 type Err = anyhow::Error;
289
290 fn from_str(s: &str) -> Result<Self, Self::Err> {
291 match s {
292 "Read" => Ok(FileOpType::Read),
293 "Write" => Ok(FileOpType::Write),
294 "Edit" => Ok(FileOpType::Edit),
295 "MultiEdit" => Ok(FileOpType::MultiEdit),
296 "Delete" => Ok(FileOpType::Delete),
297 "Glob" => Ok(FileOpType::Glob),
298 "Grep" => Ok(FileOpType::Grep),
299 _ => Err(anyhow::anyhow!("Unknown file operation type: {s}")),
300 }
301 }
302}
303
304#[derive(Debug, Serialize, Deserialize)]
306pub struct SessionAnalysis {
307 pub session_id: String,
308 pub project_path: String,
309 pub start_time: Timestamp,
310 pub end_time: Timestamp,
311 pub duration_ms: u64,
312 pub agents: Vec<AgentInvocation>,
313 pub file_operations: Vec<FileOperation>,
314 pub file_to_agents: IndexMap<String, Vec<AgentAttribution>>,
315 pub agent_stats: IndexMap<String, AgentStatistics>,
316 pub collaboration_patterns: Vec<CollaborationPattern>,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct AgentAttribution {
322 pub agent_type: String,
323 pub contribution_percent: f32,
324 pub confidence_score: f32,
325 pub operations: Vec<String>,
326 pub first_interaction: Timestamp,
327 pub last_interaction: Timestamp,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct AgentStatistics {
333 pub agent_type: String,
334 pub total_invocations: u32,
335 pub total_duration_ms: u64,
336 pub files_touched: u32,
337 pub tools_used: Vec<String>,
338 pub first_seen: Timestamp,
339 pub last_seen: Timestamp,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct CollaborationPattern {
345 pub pattern_type: String,
346 pub agents: Vec<String>,
347 pub description: String,
348 pub frequency: u32,
349 pub confidence: f32,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct AgentToolCorrelation {
355 pub agent_type: String,
356 pub tool_name: String,
357 pub usage_count: u32,
358 pub success_rate: f32,
359 pub average_invocations_per_session: f32,
360}
361
362#[derive(Debug, Serialize, Deserialize)]
364pub struct ToolAnalysis {
365 pub session_id: String,
366 pub total_tool_invocations: u32,
367 pub tool_statistics: IndexMap<String, ToolStatistics>,
368 pub agent_tool_correlations: Vec<AgentToolCorrelation>,
369 pub tool_chains: Vec<ToolChain>,
370 pub category_breakdown: IndexMap<ToolCategory, u32>,
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct ToolChain {
376 pub tools: Vec<String>,
377 pub frequency: u32,
378 pub average_time_between_ms: u64,
379 pub typical_agent: Option<String>,
380 pub success_rate: f32,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct AnalyzerConfig {
386 pub session_dirs: Vec<String>,
387 pub agent_confidence_threshold: f32,
388 pub file_attribution_window_ms: u64,
389 pub exclude_patterns: Vec<String>,
390}
391
392impl Default for AnalyzerConfig {
393 fn default() -> Self {
394 Self {
395 session_dirs: vec![],
396 agent_confidence_threshold: 0.7,
397 file_attribution_window_ms: 300_000, exclude_patterns: vec![
399 "node_modules/".to_string(),
400 "target/".to_string(),
401 ".git/".to_string(),
402 ],
403 }
404 }
405}
406
407pub fn parse_timestamp(timestamp_str: &str) -> Result<Timestamp, anyhow::Error> {
413 Timestamp::from_str(timestamp_str)
415 .map_err(|e| anyhow::anyhow!("Failed to parse timestamp '{timestamp_str}': {e}"))
416}
417
418#[must_use]
420pub fn extract_file_path(input: &serde_json::Value) -> Option<String> {
421 for field in &["file_path", "path", "pattern"] {
423 if let Some(path) = input.get(field).and_then(|v| v.as_str()) {
424 return Some(path.to_string());
425 }
426 }
427
428 if let Some(edits) = input.get("edits").and_then(|v| v.as_array()) {
430 if !edits.is_empty() {
431 if let Some(file_path) = input.get("file_path").and_then(|v| v.as_str()) {
432 return Some(file_path.to_string());
433 }
434 }
435 }
436
437 None
438}
439
440#[allow(dead_code)]
443#[must_use]
444pub fn normalize_agent_name(agent_type: &str) -> String {
445 agent_type.to_lowercase().replace(['-', ' '], "_")
446}
447
448#[allow(dead_code)]
450#[must_use]
451pub fn get_agent_category(agent_type: &str) -> &'static str {
452 match agent_type {
453 "architect" | "backend-architect" | "frontend-developer" => "architecture",
454 "developer" | "rapid-prototyper" => "development",
455 "rust-performance-expert" | "rust-code-reviewer" => "rust-expert",
456 "debugger" | "test-writer-fixer" => "testing",
457 "technical-writer" => "documentation",
458 "devops-automator" | "overseer" => "operations",
459 "general-purpose" => "general",
460 _ => "other",
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn test_parse_timestamp() {
470 let timestamp_str = "2025-10-01T09:05:21.902Z";
471 let result = parse_timestamp(timestamp_str);
472 assert!(result.is_ok());
473 }
474
475 #[test]
476 fn test_newtype_wrappers() {
477 let session_id = SessionId::new("test-session".to_string());
479 assert_eq!(session_id.as_str(), "test-session");
480 assert_eq!(session_id.to_string(), "test-session");
481 assert_eq!(session_id.as_ref(), "test-session");
482
483 let session_id_from_str: SessionId = "another-session".into();
484 assert_eq!(session_id_from_str.as_str(), "another-session");
485
486 let agent_type = AgentType::new("architect".to_string());
488 assert_eq!(agent_type.as_str(), "architect");
489 assert_eq!(agent_type.to_string(), "architect");
490
491 let message_id = MessageId::new("msg-123".to_string());
493 assert_eq!(message_id.as_str(), "msg-123");
494 assert_eq!(message_id.to_string(), "msg-123");
495 }
496
497 #[test]
498 fn test_extract_file_path() {
499 let input = serde_json::json!({
500 "file_path": "/path/to/file.rs",
501 "description": "Edit file"
502 });
503
504 let path = extract_file_path(&input);
505 assert_eq!(path, Some("/path/to/file.rs".to_string()));
506 }
507
508 #[test]
509 fn test_normalize_agent_name() {
510 assert_eq!(
511 normalize_agent_name("rust-performance-expert"),
512 "rust_performance_expert"
513 );
514 assert_eq!(
515 normalize_agent_name("backend-architect"),
516 "backend_architect"
517 );
518 }
519
520 mod proptest_tests {
521 use super::*;
522 use proptest::prelude::*;
523
524 proptest! {
525 #[test]
526 fn test_normalize_agent_name_properties(
527 input in "[a-zA-Z0-9 -]{1,50}"
528 ) {
529 let result = normalize_agent_name(&input);
530
531 prop_assert!(result.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'));
533
534 if !input.trim().is_empty() {
536 prop_assert!(!result.is_empty());
537 }
538 }
539
540 #[test]
541 fn test_parse_timestamp_properties(
542 year in 2020u16..2030,
543 month in 1u8..=12,
544 day in 1u8..=28, hour in 0u8..=23,
546 minute in 0u8..=59,
547 second in 0u8..=59,
548 millis in 0u16..1000
549 ) {
550 let timestamp_str = format!(
551 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
552 year, month, day, hour, minute, second, millis
553 );
554
555 let result = parse_timestamp(×tamp_str);
556
557 prop_assert!(result.is_ok(), "Failed to parse valid timestamp: {}", timestamp_str);
559
560 if let Ok(parsed) = result {
561 let reformatted = parsed.to_string();
563 prop_assert!(reformatted.starts_with(&year.to_string()));
564 }
565 }
566
567 #[test]
568 fn test_extract_file_path_properties(
569 file_path in r"[a-zA-Z0-9_./\-]{1,100}"
570 ) {
571 let input = serde_json::json!({
572 "file_path": file_path
573 });
574
575 let result = extract_file_path(&input);
576
577 prop_assert_eq!(result, Some(file_path.clone()));
579
580 let input_path = serde_json::json!({
582 "path": file_path
583 });
584 let result_path = extract_file_path(&input_path);
585 prop_assert_eq!(result_path, Some(file_path.clone()));
586 }
587
588 #[test]
589 fn test_newtype_wrapper_roundtrip(
590 session_id in "[a-zA-Z0-9-]{10,50}",
591 agent_type in "[a-zA-Z0-9-_]{3,30}",
592 message_id in "[a-zA-Z0-9-]{10,50}"
593 ) {
594 let session = SessionId::new(session_id.clone());
596 prop_assert_eq!(session.as_str(), &session_id);
597 prop_assert_eq!(session.to_string(), session_id);
598
599 let agent = AgentType::new(agent_type.clone());
601 prop_assert_eq!(agent.as_str(), &agent_type);
602 prop_assert_eq!(agent.to_string(), agent_type);
603
604 let message = MessageId::new(message_id.clone());
606 prop_assert_eq!(message.as_str(), &message_id);
607 prop_assert_eq!(message.to_string(), message_id);
608 }
609 }
610 }
611}