1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5use crate::common::{SessionStatus, StepType, WorkflowKind};
6use crate::version::SchemaVersion;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ConversationSession {
16 pub schema_version: SchemaVersion,
17 pub id: Uuid,
18 pub workflow: WorkflowKind,
19 pub status: SessionStatus,
20 #[serde(default)]
21 pub steps: Vec<SessionStep>,
22 #[serde(default)]
23 pub decisions: Vec<Decision>,
24 #[serde(default)]
26 pub artifact_id: Option<String>,
27 pub started_at: DateTime<Utc>,
28 #[serde(default)]
29 pub completed_at: Option<DateTime<Utc>>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct SessionStep {
35 pub step_type: StepType,
36 pub content: String,
37 pub timestamp: DateTime<Utc>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct Decision {
43 pub key: String,
44 pub value: String,
45 #[serde(default)]
46 pub rationale: Option<String>,
47}
48
49impl ConversationSession {
50 pub fn new(workflow: WorkflowKind) -> Self {
52 Self {
53 schema_version: SchemaVersion::CURRENT,
54 id: Uuid::new_v4(),
55 workflow,
56 status: SessionStatus::Active,
57 steps: Vec::new(),
58 decisions: Vec::new(),
59 artifact_id: None,
60 started_at: Utc::now(),
61 completed_at: None,
62 }
63 }
64
65 pub fn add_step(&mut self, step_type: StepType, content: String) {
67 self.steps.push(SessionStep {
68 step_type,
69 content,
70 timestamp: Utc::now(),
71 });
72 }
73
74 pub fn add_decision(&mut self, key: String, value: String, rationale: Option<String>) {
76 self.decisions.push(Decision {
77 key,
78 value,
79 rationale,
80 });
81 }
82
83 pub fn complete(&mut self, artifact_id: Option<String>) {
85 self.status = SessionStatus::Completed;
86 self.artifact_id = artifact_id;
87 self.completed_at = Some(Utc::now());
88 }
89
90 pub fn abandon(&mut self) {
92 self.status = SessionStatus::Abandoned;
93 self.completed_at = Some(Utc::now());
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn test_session_lifecycle() {
103 let mut session = ConversationSession::new(WorkflowKind::ContractNew);
104 assert_eq!(session.status, SessionStatus::Active);
105 assert!(session.completed_at.is_none());
106
107 session.add_step(StepType::Question, "What is the contract title?".to_string());
108 session.add_step(StepType::UserInput, "Key-Value Store".to_string());
109 session.add_decision(
110 "title".to_string(),
111 "Key-Value Store".to_string(),
112 Some("User provided".to_string()),
113 );
114
115 assert_eq!(session.steps.len(), 2);
116 assert_eq!(session.decisions.len(), 1);
117
118 session.complete(Some("key-value-store".to_string()));
119 assert_eq!(session.status, SessionStatus::Completed);
120 assert!(session.completed_at.is_some());
121 assert_eq!(session.artifact_id.as_deref(), Some("key-value-store"));
122 }
123
124 #[test]
125 fn test_session_json_roundtrip() {
126 let mut session = ConversationSession::new(WorkflowKind::Init);
127 session.add_step(StepType::Info, "Initializing repo".to_string());
128 session.complete(None);
129
130 let json = serde_json::to_string_pretty(&session).unwrap();
131 let parsed: ConversationSession = serde_json::from_str(&json).unwrap();
132 assert_eq!(parsed.id, session.id);
133 assert_eq!(parsed.steps.len(), 1);
134 }
135}