1use std::path::PathBuf;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use codei_llm::{Message, Role as LlmRole, ToolCall};
8
9pub type SessionId = String;
10pub type MessageId = String;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Session {
14 pub id: SessionId,
15 pub title: Option<String>,
16 pub cwd: PathBuf,
17 pub created_at: DateTime<Utc>,
18 pub updated_at: DateTime<Utc>,
19 pub messages: Vec<StoredMessage>,
20}
21
22#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
23#[serde(rename_all = "lowercase")]
24pub enum Role {
25 System,
26 User,
27 Assistant,
28 Tool,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct StoredMessage {
33 pub id: MessageId,
34 pub role: Role,
35 pub content: MessageContent,
36 pub tool_calls: Option<Vec<ToolCallRecord>>,
37 pub tool_call_id: Option<String>,
38 pub created_at: DateTime<Utc>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub enum MessageContent {
43 Text(String),
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ToolCallRecord {
48 pub id: String,
49 pub name: String,
50 pub arguments: String,
51}
52
53impl Session {
54 pub fn new(cwd: PathBuf) -> Self {
55 let now = Utc::now();
56 Self {
57 id: Uuid::new_v4().to_string(),
58 title: None,
59 cwd,
60 created_at: now,
61 updated_at: now,
62 messages: Vec::new(),
63 }
64 }
65
66 pub fn touch(&mut self) {
67 self.updated_at = Utc::now();
68 }
69
70 pub fn push_user(&mut self, content: impl Into<String>) -> &StoredMessage {
71 self.push_message(Role::User, MessageContent::Text(content.into()), None, None)
72 }
73
74 pub fn push_assistant(
75 &mut self,
76 content: String,
77 tool_calls: Option<Vec<ToolCallRecord>>,
78 ) -> &StoredMessage {
79 self.push_message(
80 Role::Assistant,
81 MessageContent::Text(content),
82 tool_calls,
83 None,
84 )
85 }
86
87 pub fn push_tool(
88 &mut self,
89 tool_call_id: impl Into<String>,
90 content: impl Into<String>,
91 ) -> &StoredMessage {
92 self.push_message(
93 Role::Tool,
94 MessageContent::Text(content.into()),
95 None,
96 Some(tool_call_id.into()),
97 )
98 }
99
100 pub fn clear_messages(&mut self) {
101 self.messages.clear();
102 self.touch();
103 }
104
105 pub fn compact(&mut self, keep_recent: usize) {
107 if self.messages.len() <= keep_recent {
108 return;
109 }
110 let remove = self.messages.len() - keep_recent;
111 self.messages.drain(0..remove);
112 self.touch();
113 }
114
115 fn push_message(
116 &mut self,
117 role: Role,
118 content: MessageContent,
119 tool_calls: Option<Vec<ToolCallRecord>>,
120 tool_call_id: Option<String>,
121 ) -> &StoredMessage {
122 let msg = StoredMessage {
123 id: Uuid::new_v4().to_string(),
124 role,
125 content,
126 tool_calls,
127 tool_call_id,
128 created_at: Utc::now(),
129 };
130 self.messages.push(msg);
131 self.touch();
132 self.messages.last().expect("message just pushed")
133 }
134}
135
136impl From<ToolCall> for ToolCallRecord {
137 fn from(value: ToolCall) -> Self {
138 Self {
139 id: value.id,
140 name: value.name,
141 arguments: value.arguments,
142 }
143 }
144}
145
146impl StoredMessage {
147 pub fn text(&self) -> Option<&str> {
148 match &self.content {
149 MessageContent::Text(s) => Some(s),
150 }
151 }
152}
153
154pub fn to_llm_messages(session: &Session, system_prompt: &str) -> Vec<Message> {
155 let mut messages = vec![Message::system(system_prompt)];
156 for msg in &session.messages {
157 match msg.role {
158 Role::User => {
159 if let Some(text) = msg.text() {
160 messages.push(Message::user(text));
161 }
162 }
163 Role::Assistant => {
164 let tool_calls: Option<Vec<ToolCall>> = msg.tool_calls.as_ref().map(|calls| {
165 calls
166 .iter()
167 .map(|c| ToolCall {
168 id: c.id.clone(),
169 name: c.name.clone(),
170 arguments: c.arguments.clone(),
171 })
172 .collect()
173 });
174 let text = msg.text().unwrap_or("").to_string();
175 if let Some(calls) = &tool_calls {
176 if !calls.is_empty() {
177 messages.push(Message {
178 role: LlmRole::Assistant,
179 content: if text.is_empty() { None } else { Some(text) },
180 tool_calls,
181 tool_call_id: None,
182 });
183 } else {
184 messages.push(Message::assistant(text, None));
185 }
186 } else {
187 messages.push(Message::assistant(text, None));
188 }
189 }
190 Role::Tool => {
191 if let (Some(id), Some(text)) = (&msg.tool_call_id, msg.text()) {
192 messages.push(Message::tool(id.clone(), text));
193 }
194 }
195 Role::System => {}
196 }
197 }
198 messages
199}