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