1use std::path::{Path, PathBuf};
4
5use chrono::{DateTime, Utc};
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use super::state::{MessageId, SessionId};
11
12#[derive(Clone, Debug, Default, Serialize, Deserialize)]
14pub struct EnvironmentContext {
15 pub cwd: Option<PathBuf>,
16 pub git_branch: Option<String>,
17 pub git_commit: Option<String>,
18 pub platform: Option<String>,
19 pub sdk_version: Option<String>,
20}
21
22impl EnvironmentContext {
23 pub fn capture(working_dir: Option<&Path>) -> Self {
24 let (git_branch, git_commit) = working_dir.map(Self::git_info).unwrap_or_default();
25
26 Self {
27 cwd: working_dir.map(|p| p.to_path_buf()),
28 git_branch,
29 git_commit,
30 platform: Some(current_platform().to_string()),
31 sdk_version: Some(env!("CARGO_PKG_VERSION").to_string()),
32 }
33 }
34
35 fn git_info(dir: &Path) -> (Option<String>, Option<String>) {
36 let branch = std::process::Command::new("git")
37 .args(["rev-parse", "--abbrev-ref", "HEAD"])
38 .current_dir(dir)
39 .output()
40 .ok()
41 .filter(|o| o.status.success())
42 .and_then(|o| String::from_utf8(o.stdout).ok())
43 .map(|s| s.trim().to_string())
44 .filter(|s| !s.is_empty());
45
46 let commit = std::process::Command::new("git")
47 .args(["rev-parse", "--short", "HEAD"])
48 .current_dir(dir)
49 .output()
50 .ok()
51 .filter(|o| o.status.success())
52 .and_then(|o| String::from_utf8(o.stdout).ok())
53 .map(|s| s.trim().to_string())
54 .filter(|s| !s.is_empty());
55
56 (branch, commit)
57 }
58
59 pub fn is_empty(&self) -> bool {
60 self.cwd.is_none() && self.git_branch.is_none()
61 }
62}
63
64fn current_platform() -> &'static str {
65 if cfg!(target_os = "macos") {
66 "darwin"
67 } else if cfg!(target_os = "linux") {
68 "linux"
69 } else if cfg!(target_os = "windows") {
70 "windows"
71 } else {
72 "unknown"
73 }
74}
75
76#[derive(Clone, Debug, Serialize, Deserialize)]
78pub struct ToolExecution {
79 pub id: Uuid,
80 pub session_id: SessionId,
81 pub message_id: Option<String>,
82 pub tool_name: String,
83 pub tool_input: serde_json::Value,
84 pub tool_output: String,
85 pub is_error: bool,
86 pub error_message: Option<String>,
87 pub duration_ms: u64,
88 pub input_tokens: Option<u32>,
89 pub output_tokens: Option<u32>,
90 pub plan_id: Option<Uuid>,
91 pub created_at: DateTime<Utc>,
92}
93
94impl ToolExecution {
95 pub fn new(
96 session_id: SessionId,
97 tool_name: impl Into<String>,
98 tool_input: serde_json::Value,
99 ) -> Self {
100 Self {
101 id: Uuid::new_v4(),
102 session_id,
103 message_id: None,
104 tool_name: tool_name.into(),
105 tool_input,
106 tool_output: String::new(),
107 is_error: false,
108 error_message: None,
109 duration_ms: 0,
110 input_tokens: None,
111 output_tokens: None,
112 plan_id: None,
113 created_at: Utc::now(),
114 }
115 }
116
117 pub fn output(mut self, output: impl Into<String>, is_error: bool) -> Self {
118 self.tool_output = output.into();
119 self.is_error = is_error;
120 self
121 }
122
123 pub fn error(mut self, message: impl Into<String>) -> Self {
124 self.is_error = true;
125 self.error_message = Some(message.into());
126 self
127 }
128
129 pub fn duration(mut self, duration_ms: u64) -> Self {
130 self.duration_ms = duration_ms;
131 self
132 }
133
134 pub fn plan(mut self, plan_id: Uuid) -> Self {
135 self.plan_id = Some(plan_id);
136 self
137 }
138
139 pub fn message(mut self, message_id: impl Into<String>) -> Self {
140 self.message_id = Some(message_id.into());
141 self
142 }
143
144 pub fn tokens(mut self, input: u32, output: u32) -> Self {
145 self.input_tokens = Some(input);
146 self.output_tokens = Some(output);
147 self
148 }
149}
150
151#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "snake_case")]
153pub enum PlanStatus {
154 #[default]
155 Draft,
156 Approved,
157 Executing,
158 Completed,
159 Failed,
160 Cancelled,
161}
162
163impl PlanStatus {
164 pub fn is_terminal(&self) -> bool {
165 matches!(self, Self::Completed | Self::Failed | Self::Cancelled)
166 }
167
168 pub fn can_execute(&self) -> bool {
169 matches!(self, Self::Approved)
170 }
171
172 pub fn from_str_lenient(s: &str) -> Self {
174 match s.to_lowercase().as_str() {
175 "approved" => Self::Approved,
176 "executing" | "inprogress" | "in_progress" => Self::Executing,
177 "completed" => Self::Completed,
178 "cancelled" | "canceled" => Self::Cancelled,
179 "failed" => Self::Failed,
180 _ => Self::Draft,
181 }
182 }
183}
184
185#[derive(Clone, Debug, Serialize, Deserialize)]
186pub struct Plan {
187 pub id: Uuid,
188 pub session_id: SessionId,
189 pub name: Option<String>,
190 pub content: String,
191 pub status: PlanStatus,
192 pub error: Option<String>,
193 pub created_at: DateTime<Utc>,
194 pub approved_at: Option<DateTime<Utc>>,
195 pub started_at: Option<DateTime<Utc>>,
196 pub completed_at: Option<DateTime<Utc>>,
197}
198
199impl Plan {
200 pub fn new(session_id: SessionId) -> Self {
201 Self {
202 id: Uuid::new_v4(),
203 session_id,
204 name: None,
205 content: String::new(),
206 status: PlanStatus::Draft,
207 error: None,
208 created_at: Utc::now(),
209 approved_at: None,
210 started_at: None,
211 completed_at: None,
212 }
213 }
214
215 pub fn name(mut self, name: impl Into<String>) -> Self {
216 self.name = Some(name.into());
217 self
218 }
219
220 pub fn content(mut self, content: impl Into<String>) -> Self {
221 self.content = content.into();
222 self
223 }
224
225 pub fn approve(&mut self) {
226 self.status = PlanStatus::Approved;
227 self.approved_at = Some(Utc::now());
228 }
229
230 pub fn start_execution(&mut self) {
231 self.status = PlanStatus::Executing;
232 self.started_at = Some(Utc::now());
233 }
234
235 pub fn complete(&mut self) {
236 self.status = PlanStatus::Completed;
237 self.completed_at = Some(Utc::now());
238 }
239
240 pub fn fail(&mut self, error: impl Into<String>) {
241 self.status = PlanStatus::Failed;
242 self.completed_at = Some(Utc::now());
243 self.error = Some(error.into());
244 }
245
246 pub fn cancel(&mut self) {
247 self.status = PlanStatus::Cancelled;
248 self.completed_at = Some(Utc::now());
249 }
250}
251
252#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
253#[serde(rename_all = "snake_case")]
254pub enum TodoStatus {
255 #[default]
256 Pending,
257 InProgress,
258 Completed,
259}
260
261impl TodoStatus {
262 pub fn from_str_lenient(s: &str) -> Self {
264 match s.to_lowercase().as_str() {
265 "in_progress" | "inprogress" => Self::InProgress,
266 "completed" | "done" => Self::Completed,
267 _ => Self::Pending,
268 }
269 }
270}
271
272#[derive(Clone, Debug, Serialize, Deserialize)]
273pub struct TodoItem {
274 pub id: Uuid,
275 pub session_id: SessionId,
276 pub content: String,
277 pub active_form: String,
278 pub status: TodoStatus,
279 pub plan_id: Option<Uuid>,
280 pub created_at: DateTime<Utc>,
281 pub started_at: Option<DateTime<Utc>>,
282 pub completed_at: Option<DateTime<Utc>>,
283}
284
285impl TodoItem {
286 pub fn new(
287 session_id: SessionId,
288 content: impl Into<String>,
289 active_form: impl Into<String>,
290 ) -> Self {
291 Self {
292 id: Uuid::new_v4(),
293 session_id,
294 content: content.into(),
295 active_form: active_form.into(),
296 status: TodoStatus::Pending,
297 plan_id: None,
298 created_at: Utc::now(),
299 started_at: None,
300 completed_at: None,
301 }
302 }
303
304 pub fn plan(mut self, plan_id: Uuid) -> Self {
305 self.plan_id = Some(plan_id);
306 self
307 }
308
309 pub fn start(&mut self) {
310 self.status = TodoStatus::InProgress;
311 self.started_at = Some(Utc::now());
312 }
313
314 pub fn complete(&mut self) {
315 self.status = TodoStatus::Completed;
316 self.completed_at = Some(Utc::now());
317 }
318
319 pub fn status_icon(&self) -> &'static str {
320 match self.status {
321 TodoStatus::Pending => "○",
322 TodoStatus::InProgress => "◐",
323 TodoStatus::Completed => "●",
324 }
325 }
326}
327
328#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
329#[serde(rename_all = "snake_case")]
330pub enum CompactTrigger {
331 #[default]
332 Manual,
333 Auto,
334 Threshold,
335}
336
337impl CompactTrigger {
338 pub fn from_str_lenient(s: &str) -> Self {
340 match s.to_lowercase().as_str() {
341 "auto" | "automatic" => Self::Auto,
342 "threshold" => Self::Threshold,
343 _ => Self::Manual,
344 }
345 }
346}
347
348#[derive(Clone, Debug, Serialize, Deserialize)]
349pub struct CompactRecord {
350 pub id: Uuid,
351 pub session_id: SessionId,
352 pub trigger: CompactTrigger,
353 pub pre_tokens: usize,
354 pub post_tokens: usize,
355 pub saved_tokens: usize,
356 pub summary: String,
357 pub original_count: usize,
358 pub new_count: usize,
359 pub logical_parent_id: Option<MessageId>,
360 pub created_at: DateTime<Utc>,
361}
362
363impl CompactRecord {
364 pub fn new(session_id: SessionId) -> Self {
365 Self {
366 id: Uuid::new_v4(),
367 session_id,
368 trigger: CompactTrigger::default(),
369 pre_tokens: 0,
370 post_tokens: 0,
371 saved_tokens: 0,
372 summary: String::new(),
373 original_count: 0,
374 new_count: 0,
375 logical_parent_id: None,
376 created_at: Utc::now(),
377 }
378 }
379
380 pub fn trigger(mut self, trigger: CompactTrigger) -> Self {
381 self.trigger = trigger;
382 self
383 }
384
385 pub fn counts(mut self, original: usize, new: usize) -> Self {
386 self.original_count = original;
387 self.new_count = new;
388 self
389 }
390
391 pub fn summary(mut self, summary: impl Into<String>) -> Self {
392 self.summary = summary.into();
393 self
394 }
395
396 pub fn saved_tokens(mut self, saved: usize) -> Self {
397 self.saved_tokens = saved;
398 self
399 }
400
401 pub fn tokens(mut self, pre: usize, post: usize) -> Self {
402 self.pre_tokens = pre;
403 self.post_tokens = post;
404 self.saved_tokens = pre.saturating_sub(post);
405 self
406 }
407
408 pub fn logical_parent(mut self, parent_id: MessageId) -> Self {
409 self.logical_parent_id = Some(parent_id);
410 self
411 }
412}
413
414#[derive(Clone, Debug, Serialize, Deserialize)]
415pub struct SummarySnapshot {
416 pub id: Uuid,
417 pub session_id: SessionId,
418 pub summary: String,
419 pub leaf_message_id: Option<MessageId>,
420 pub created_at: DateTime<Utc>,
421}
422
423impl SummarySnapshot {
424 pub fn new(session_id: SessionId, summary: impl Into<String>) -> Self {
425 Self {
426 id: Uuid::new_v4(),
427 session_id,
428 summary: summary.into(),
429 leaf_message_id: None,
430 created_at: Utc::now(),
431 }
432 }
433
434 pub fn leaf(mut self, leaf_id: MessageId) -> Self {
435 self.leaf_message_id = Some(leaf_id);
436 self
437 }
438}
439
440#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
441#[serde(rename_all = "snake_case")]
442pub enum QueueOperation {
443 Enqueue,
444}
445
446#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
447#[serde(rename_all = "snake_case")]
448pub enum QueueStatus {
449 #[default]
450 Pending,
451 Processing,
452 Completed,
453 Cancelled,
454}
455
456#[derive(Clone, Debug, Serialize, Deserialize)]
457pub struct QueueItem {
458 pub id: Uuid,
459 pub session_id: SessionId,
460 pub operation: QueueOperation,
461 pub content: String,
462 pub priority: i32,
463 pub status: QueueStatus,
464 pub created_at: DateTime<Utc>,
465 pub processed_at: Option<DateTime<Utc>>,
466}
467
468impl QueueItem {
469 pub fn enqueue(session_id: SessionId, content: impl Into<String>) -> Self {
470 Self {
471 id: Uuid::new_v4(),
472 session_id,
473 operation: QueueOperation::Enqueue,
474 content: content.into(),
475 priority: 0,
476 status: QueueStatus::Pending,
477 created_at: Utc::now(),
478 processed_at: None,
479 }
480 }
481
482 pub fn priority(mut self, priority: i32) -> Self {
483 self.priority = priority;
484 self
485 }
486
487 pub fn start_processing(&mut self) {
488 self.status = QueueStatus::Processing;
489 }
490
491 pub fn complete(&mut self) {
492 self.status = QueueStatus::Completed;
493 self.processed_at = Some(Utc::now());
494 }
495
496 pub fn cancel(&mut self) {
497 self.status = QueueStatus::Cancelled;
498 self.processed_at = Some(Utc::now());
499 }
500}
501
502#[derive(Clone, Debug, Default, Serialize, Deserialize)]
503pub struct SessionStats {
504 pub total_messages: usize,
505 pub total_tool_calls: usize,
506 pub tool_success_count: usize,
507 pub tool_error_count: usize,
508 pub total_input_tokens: u64,
509 pub total_output_tokens: u64,
510 pub total_cost_usd: Decimal,
511 pub avg_tool_duration_ms: f64,
512 pub plans_count: usize,
513 pub todos_completed: usize,
514 pub todos_total: usize,
515 pub compacts_count: usize,
516 pub subagent_count: usize,
517}
518
519impl SessionStats {
520 pub fn tool_success_rate(&self) -> f64 {
521 if self.total_tool_calls == 0 {
522 1.0
523 } else {
524 self.tool_success_count as f64 / self.total_tool_calls as f64
525 }
526 }
527
528 pub fn total_tokens(&self) -> u64 {
529 self.total_input_tokens + self.total_output_tokens
530 }
531}
532
533#[derive(Clone, Debug, Serialize, Deserialize)]
534pub struct SessionTree {
535 pub session_id: SessionId,
536 pub session_type: super::state::SessionType,
537 pub stats: SessionStats,
538 pub children: Vec<SessionTree>,
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544
545 #[test]
546 fn test_environment_context() {
547 let ctx = EnvironmentContext::capture(None);
548 assert!(ctx.cwd.is_none());
549 assert!(ctx.platform.is_some());
550 assert!(ctx.sdk_version.is_some());
551 }
552
553 #[test]
554 fn test_tool_execution_builder() {
555 let session_id = SessionId::new();
556 let exec = ToolExecution::new(session_id, "Bash", serde_json::json!({"command": "ls"}))
557 .output("file1\nfile2", false)
558 .duration(150);
559
560 assert_eq!(exec.tool_name, "Bash");
561 assert_eq!(exec.duration_ms, 150);
562 assert!(!exec.is_error);
563 }
564
565 #[test]
566 fn test_plan_lifecycle() {
567 let session_id = SessionId::new();
568 let mut plan = Plan::new(session_id)
569 .name("Implement auth")
570 .content("1. Create user model\n2. Add endpoints");
571
572 assert_eq!(plan.status, PlanStatus::Draft);
573
574 plan.approve();
575 assert_eq!(plan.status, PlanStatus::Approved);
576 assert!(plan.approved_at.is_some());
577
578 plan.start_execution();
579 assert_eq!(plan.status, PlanStatus::Executing);
580
581 plan.complete();
582 assert_eq!(plan.status, PlanStatus::Completed);
583 assert!(plan.status.is_terminal());
584 }
585
586 #[test]
587 fn test_todo_item() {
588 let session_id = SessionId::new();
589 let mut todo = TodoItem::new(session_id, "Fix bug", "Fixing bug");
590
591 assert_eq!(todo.status, TodoStatus::Pending);
592 assert_eq!(todo.status_icon(), "○");
593
594 todo.start();
595 assert_eq!(todo.status, TodoStatus::InProgress);
596 assert_eq!(todo.status_icon(), "◐");
597
598 todo.complete();
599 assert_eq!(todo.status, TodoStatus::Completed);
600 assert_eq!(todo.status_icon(), "●");
601 }
602
603 #[test]
604 fn test_compact_record() {
605 let session_id = SessionId::new();
606 let record = CompactRecord::new(session_id)
607 .trigger(CompactTrigger::Threshold)
608 .tokens(100_000, 20_000)
609 .counts(50, 5)
610 .summary("Summary of conversation");
611
612 assert_eq!(record.pre_tokens, 100_000);
613 assert_eq!(record.post_tokens, 20_000);
614 assert_eq!(record.saved_tokens, 80_000);
615 assert_eq!(record.original_count, 50);
616 assert_eq!(record.new_count, 5);
617 }
618
619 #[test]
620 fn test_summary_snapshot() {
621 let session_id = SessionId::new();
622 let snapshot = SummarySnapshot::new(session_id, "Working on feature X");
623
624 assert!(!snapshot.summary.is_empty());
625 assert!(snapshot.leaf_message_id.is_none());
626 }
627
628 #[test]
629 fn test_queue_item() {
630 let session_id = SessionId::new();
631 let mut item = QueueItem::enqueue(session_id, "Process this").priority(10);
632
633 assert_eq!(item.status, QueueStatus::Pending);
634 assert_eq!(item.priority, 10);
635
636 item.start_processing();
637 assert_eq!(item.status, QueueStatus::Processing);
638
639 item.complete();
640 assert_eq!(item.status, QueueStatus::Completed);
641 assert!(item.processed_at.is_some());
642 }
643
644 #[test]
645 fn test_session_stats() {
646 let stats = SessionStats {
647 total_tool_calls: 10,
648 tool_success_count: 8,
649 tool_error_count: 2,
650 total_input_tokens: 1000,
651 total_output_tokens: 500,
652 ..Default::default()
653 };
654
655 assert!((stats.tool_success_rate() - 0.8).abs() < 0.001);
656 assert_eq!(stats.total_tokens(), 1500);
657 }
658
659 #[test]
660 fn test_status_from_str_lenient() {
661 assert_eq!(
663 TodoStatus::from_str_lenient("in_progress"),
664 TodoStatus::InProgress
665 );
666 assert_eq!(
667 TodoStatus::from_str_lenient("inprogress"),
668 TodoStatus::InProgress
669 );
670 assert_eq!(
671 TodoStatus::from_str_lenient("completed"),
672 TodoStatus::Completed
673 );
674 assert_eq!(TodoStatus::from_str_lenient("unknown"), TodoStatus::Pending);
675
676 assert_eq!(
678 PlanStatus::from_str_lenient("approved"),
679 PlanStatus::Approved
680 );
681 assert_eq!(
682 PlanStatus::from_str_lenient("executing"),
683 PlanStatus::Executing
684 );
685 assert_eq!(
686 PlanStatus::from_str_lenient("inprogress"),
687 PlanStatus::Executing
688 );
689 assert_eq!(
690 PlanStatus::from_str_lenient("cancelled"),
691 PlanStatus::Cancelled
692 );
693 assert_eq!(PlanStatus::from_str_lenient("unknown"), PlanStatus::Draft);
694
695 assert_eq!(
697 CompactTrigger::from_str_lenient("auto"),
698 CompactTrigger::Auto
699 );
700 assert_eq!(
701 CompactTrigger::from_str_lenient("automatic"),
702 CompactTrigger::Auto
703 );
704 assert_eq!(
705 CompactTrigger::from_str_lenient("threshold"),
706 CompactTrigger::Threshold
707 );
708 assert_eq!(
709 CompactTrigger::from_str_lenient("unknown"),
710 CompactTrigger::Manual
711 );
712 }
713}