Skip to main content

lexicon_spec/
session.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5use crate::common::{SessionStatus, StepType, WorkflowKind};
6use crate::version::SchemaVersion;
7
8/// A conversation session record.
9///
10/// Records the steps, decisions, and context of a conversational
11/// artifact creation workflow. Stored for reuse in future generations.
12///
13/// Stored at `.lexicon/conversations/<uuid>.json`.
14#[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    /// The artifact ID produced by this session, if any.
25    #[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/// A single step in a conversation workflow.
33#[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/// A decision recorded during a conversation.
41#[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    /// Create a new active session for the given workflow.
51    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    /// Add a step to the session.
66    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    /// Record a decision.
75    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    /// Mark the session as completed.
84    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    /// Mark the session as abandoned.
91    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}