1use std::sync::Mutex;
4
5use chrono::Utc;
6use rusqlite::{params, Connection};
7use uuid::Uuid;
8
9use a2a::message::{Message, MessageKind, MsgState, Part};
10use a2a::task::{Task, TaskState};
11use substrate_core::mailbox_port::{MailboxStore, MailboxTaskState};
12
13use crate::error::StoreError;
14use crate::schema;
15
16pub struct SqliteMailboxStore {
21 conn: Mutex<Connection>,
22}
23
24impl SqliteMailboxStore {
25 pub fn open(path: &str) -> Result<Self, StoreError> {
27 let conn = Connection::open(path)?;
28 schema::init(&conn)?;
29 Ok(Self {
30 conn: Mutex::new(conn),
31 })
32 }
33
34 pub fn open_in_memory() -> Result<Self, StoreError> {
36 let conn = Connection::open_in_memory()?;
37 schema::init(&conn)?;
38 Ok(Self {
39 conn: Mutex::new(conn),
40 })
41 }
42}
43
44fn task_state_to_str(s: MailboxTaskState) -> &'static str {
47 match s {
48 MailboxTaskState::Submitted => "submitted",
49 MailboxTaskState::Working => "working",
50 MailboxTaskState::InputRequired => "input_required",
51 MailboxTaskState::Completed => "completed",
52 MailboxTaskState::Failed => "failed",
53 MailboxTaskState::Cancelled => "cancelled",
54 }
55}
56
57fn str_to_task_state(s: &str) -> TaskState {
58 match s {
59 "working" => TaskState::Working,
60 "input_required" => TaskState::InputRequired,
61 "completed" => TaskState::Completed,
62 "failed" => TaskState::Failed,
63 "cancelled" => TaskState::Cancelled,
64 _ => TaskState::Submitted,
65 }
66}
67
68fn msg_kind_to_str(k: &MessageKind) -> &'static str {
69 match k {
70 MessageKind::Task => "task",
71 MessageKind::Reply => "reply",
72 MessageKind::Question => "question",
73 MessageKind::Status => "status",
74 MessageKind::Artifact => "artifact",
75 }
76}
77
78fn str_to_msg_kind(s: &str) -> MessageKind {
79 match s {
80 "reply" => MessageKind::Reply,
81 "question" => MessageKind::Question,
82 "status" => MessageKind::Status,
83 "artifact" => MessageKind::Artifact,
84 _ => MessageKind::Task,
85 }
86}
87
88fn a2a_task_state_to_str(s: TaskState) -> &'static str {
89 match s {
90 TaskState::Submitted => "submitted",
91 TaskState::Working => "working",
92 TaskState::InputRequired => "input_required",
93 TaskState::Completed => "completed",
94 TaskState::Failed => "failed",
95 TaskState::Cancelled => "cancelled",
96 }
97}
98
99impl MailboxStore for SqliteMailboxStore {
102 type Msg = Message;
103 type Task = Task;
104 type Error = StoreError;
105
106 fn post(&self, msg: &Message) -> Result<(), StoreError> {
107 let conn = self.conn.lock().unwrap();
108 let parts_json = serde_json::to_string(&msg.parts)?;
109 conn.execute(
110 "INSERT INTO mailbox \
111 (id, team_id, task_id, from_agent, to_agent, kind, parts, in_reply_to, state, created_at, consumed_at) \
112 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
113 params![
114 msg.id.to_string(),
115 msg.team_id,
116 msg.task_id.map(|u| u.to_string()),
117 msg.from,
118 msg.to,
119 msg_kind_to_str(&msg.kind),
120 parts_json,
121 msg.in_reply_to.map(|u| u.to_string()),
122 "unread",
123 msg.created_at.to_rfc3339(),
124 msg.consumed_at.map(|dt| dt.to_rfc3339()),
125 ],
126 )?;
127 Ok(())
128 }
129
130 fn inbox(&self, team_id: &str, to: &str) -> Result<Vec<Message>, StoreError> {
131 let conn = self.conn.lock().unwrap();
132 let mut stmt = conn.prepare(
133 "SELECT id, team_id, task_id, from_agent, to_agent, kind, parts, \
134 in_reply_to, state, created_at, consumed_at \
135 FROM mailbox \
136 WHERE team_id=?1 AND to_agent=?2 AND state='unread' \
137 ORDER BY created_at ASC",
138 )?;
139 let rows: Vec<Message> = stmt
140 .query_map(params![team_id, to], |row| {
141 Ok((
142 row.get::<_, String>(0)?,
143 row.get::<_, String>(1)?,
144 row.get::<_, Option<String>>(2)?,
145 row.get::<_, String>(3)?,
146 row.get::<_, String>(4)?,
147 row.get::<_, String>(5)?,
148 row.get::<_, String>(6)?,
149 row.get::<_, Option<String>>(7)?,
150 row.get::<_, String>(8)?,
151 row.get::<_, String>(9)?,
152 row.get::<_, Option<String>>(10)?,
153 ))
154 })?
155 .filter_map(|r| r.ok())
156 .map(
157 |(
158 id_str,
159 team_id,
160 task_id_str,
161 from,
162 to,
163 kind_str,
164 parts_json,
165 in_reply_str,
166 state_str,
167 created_str,
168 consumed_str,
169 )| {
170 let parts: Vec<Part> = serde_json::from_str(&parts_json).unwrap_or_default();
171 let state = match state_str.as_str() {
172 "delivered" => MsgState::Delivered,
173 "consumed" => MsgState::Consumed,
174 _ => MsgState::Unread,
175 };
176 Message {
177 id: Uuid::parse_str(&id_str).unwrap_or_else(|_| Uuid::new_v4()),
178 team_id,
179 task_id: task_id_str.and_then(|s| Uuid::parse_str(&s).ok()),
180 from,
181 to,
182 kind: str_to_msg_kind(&kind_str),
183 parts,
184 in_reply_to: in_reply_str.and_then(|s| Uuid::parse_str(&s).ok()),
185 state,
186 created_at: created_str.parse().unwrap_or_else(|_| Utc::now()),
187 consumed_at: consumed_str.and_then(|s| s.parse().ok()),
188 }
189 },
190 )
191 .collect();
192 Ok(rows)
193 }
194
195 fn claim(&self, message_id: Uuid) -> Result<bool, StoreError> {
196 let conn = self.conn.lock().unwrap();
197 let n = conn.execute(
198 "UPDATE mailbox SET state='delivered' WHERE id=?1 AND state='unread'",
199 params![message_id.to_string()],
200 )?;
201 Ok(n == 1)
202 }
203
204 fn consume(&self, message_id: Uuid) -> Result<(), StoreError> {
205 let conn = self.conn.lock().unwrap();
206 conn.execute(
207 "UPDATE mailbox SET state='consumed', consumed_at=?2 WHERE id=?1",
208 params![message_id.to_string(), Utc::now().to_rfc3339()],
209 )?;
210 Ok(())
211 }
212
213 fn unclaim(&self, message_id: Uuid) -> Result<(), StoreError> {
214 let conn = self.conn.lock().unwrap();
215 conn.execute(
216 "UPDATE mailbox SET state='unread', consumed_at=NULL WHERE id=?1 AND state='delivered'",
217 params![message_id.to_string()],
218 )?;
219 Ok(())
220 }
221
222 fn task_create(&self, task: &Task) -> Result<(), StoreError> {
223 let conn = self.conn.lock().unwrap();
224 conn.execute(
225 "INSERT INTO tasklist \
226 (id, team_id, title, state, owner, parent_task_id, requirement_id, epic_id, created_at, updated_at) \
227 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
228 params![
229 task.id.to_string(),
230 task.team_id,
231 task.title,
232 a2a_task_state_to_str(task.state),
233 task.owner,
234 task.parent_task_id.map(|u| u.to_string()),
235 task.requirement_id,
236 task.epic_id,
237 task.created_at.to_rfc3339(),
238 task.updated_at.to_rfc3339(),
239 ],
240 )?;
241 Ok(())
242 }
243
244 fn task_update(
245 &self,
246 id: Uuid,
247 state: MailboxTaskState,
248 note: Option<&str>,
249 ) -> Result<(), StoreError> {
250 let conn = self.conn.lock().unwrap();
251 conn.execute(
252 "UPDATE tasklist SET state=?2, updated_at=?3, note=?4 WHERE id=?1",
253 params![
254 id.to_string(),
255 task_state_to_str(state),
256 Utc::now().to_rfc3339(),
257 note,
258 ],
259 )?;
260 Ok(())
261 }
262
263 fn task_list(&self, team_id: &str) -> Result<Vec<Task>, StoreError> {
264 let conn = self.conn.lock().unwrap();
265 let mut stmt = conn.prepare(
266 "SELECT id, team_id, title, state, owner, parent_task_id, requirement_id, epic_id, created_at, updated_at \
267 FROM tasklist WHERE team_id=?1 ORDER BY created_at ASC",
268 )?;
269 let tasks: Vec<Task> = stmt
270 .query_map(params![team_id], |row| {
271 Ok((
272 row.get::<_, String>(0)?,
273 row.get::<_, String>(1)?,
274 row.get::<_, String>(2)?,
275 row.get::<_, String>(3)?,
276 row.get::<_, String>(4)?,
277 row.get::<_, Option<String>>(5)?,
278 row.get::<_, Option<String>>(6)?,
279 row.get::<_, Option<String>>(7)?,
280 row.get::<_, String>(8)?,
281 row.get::<_, String>(9)?,
282 ))
283 })?
284 .filter_map(|r| r.ok())
285 .map(
286 |(
287 id_str,
288 team_id,
289 title,
290 state_str,
291 owner,
292 parent_str,
293 req_id,
294 epic_id,
295 created_str,
296 updated_str,
297 )| Task {
298 id: Uuid::parse_str(&id_str).unwrap_or_else(|_| Uuid::new_v4()),
299 team_id,
300 title,
301 state: str_to_task_state(&state_str),
302 owner,
303 parent_task_id: parent_str.and_then(|s| Uuid::parse_str(&s).ok()),
304 requirement_id: req_id,
305 epic_id,
306 created_at: created_str.parse().unwrap_or_else(|_| Utc::now()),
307 updated_at: updated_str.parse().unwrap_or_else(|_| Utc::now()),
308 },
309 )
310 .collect();
311 Ok(tasks)
312 }
313}