1use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21#[non_exhaustive]
22pub struct RecallMessage {
23 #[serde(default)]
26 pub id: i64,
27 pub role: String,
29 pub content: String,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub tool_name: Option<String>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub tool_calls: Option<String>,
37 pub ts_ms: i64,
39}
40
41impl RecallMessage {
42 pub fn new(role: impl Into<String>, content: impl Into<String>, ts_ms: i64) -> Self {
43 Self {
44 id: 0,
45 role: role.into(),
46 content: content.into(),
47 tool_name: None,
48 tool_calls: None,
49 ts_ms,
50 }
51 }
52 pub fn with_tool_name(mut self, name: impl Into<String>) -> Self {
53 self.tool_name = Some(name.into());
54 self
55 }
56 pub fn with_tool_calls(mut self, calls: impl Into<String>) -> Self {
57 self.tool_calls = Some(calls.into());
58 self
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64#[non_exhaustive]
65pub struct SessionMeta {
66 pub session_id: String,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub title: Option<String>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub source: Option<String>,
72 pub started_at_ms: i64,
73 #[serde(default)]
74 pub message_count: i64,
75}
76
77impl SessionMeta {
78 pub fn new(session_id: impl Into<String>, started_at_ms: i64) -> Self {
79 Self {
80 session_id: session_id.into(),
81 title: None,
82 source: None,
83 started_at_ms,
84 message_count: 0,
85 }
86 }
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
92#[non_exhaustive]
93pub struct SessionHit {
94 pub session: SessionMeta,
95 pub snippet: String,
97 pub anchor_id: i64,
99 pub bookend_start: Vec<RecallMessage>,
101 pub around: Vec<RecallMessage>,
103 pub bookend_end: Vec<RecallMessage>,
105}
106
107impl SessionHit {
108 pub fn new(
109 session: SessionMeta,
110 snippet: String,
111 anchor_id: i64,
112 bookend_start: Vec<RecallMessage>,
113 around: Vec<RecallMessage>,
114 bookend_end: Vec<RecallMessage>,
115 ) -> Self {
116 Self {
117 session,
118 snippet,
119 anchor_id,
120 bookend_start,
121 around,
122 bookend_end,
123 }
124 }
125}
126
127#[derive(Debug, thiserror::Error)]
128#[non_exhaustive]
129pub enum RecallError {
130 #[error("recall io: {0}")]
131 Io(String),
132 #[error("recall backend: {0}")]
133 Backend(String),
134 #[error("recall serde: {0}")]
135 Serde(String),
136 #[error("not found: {0}")]
137 NotFound(String),
138}
139
140#[async_trait::async_trait]
143pub trait RecallStore: Send + Sync + 'static {
144 async fn ensure_session(
146 &self,
147 owner: &str,
148 session_id: &str,
149 meta: &SessionMeta,
150 ) -> Result<(), RecallError>;
151
152 async fn append(
154 &self,
155 owner: &str,
156 session_id: &str,
157 msg: &RecallMessage,
158 ) -> Result<i64, RecallError>;
159
160 async fn search(
162 &self,
163 owner: &str,
164 query: &str,
165 limit: usize,
166 ) -> Result<Vec<SessionHit>, RecallError>;
167
168 async fn scroll(
170 &self,
171 owner: &str,
172 session_id: &str,
173 around: i64,
174 window: usize,
175 ) -> Result<Vec<RecallMessage>, RecallError>;
176
177 async fn recent(&self, owner: &str, limit: usize) -> Result<Vec<SessionMeta>, RecallError>;
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn types_round_trip_through_serde() {
187 let m = RecallMessage::new("assistant", "hello", 123).with_tool_calls("[]");
188 let j = serde_json::to_string(&m).unwrap();
189 let back: RecallMessage = serde_json::from_str(&j).unwrap();
190 assert_eq!(back.role, "assistant");
191 assert_eq!(back.tool_calls.as_deref(), Some("[]"));
192 assert!(back.tool_name.is_none());
193
194 let hit = SessionHit {
195 session: SessionMeta::new("s1", 1),
196 snippet: ">>>hi<<<".into(),
197 anchor_id: 1,
198 bookend_start: vec![m.clone()],
199 around: vec![m.clone()],
200 bookend_end: vec![m],
201 };
202 let j = serde_json::to_string(&hit).unwrap();
203 assert!(j.contains("\"anchor_id\":1"));
204 }
205}