1use std::fmt;
2use std::str::FromStr;
3
4use anyhow::Result;
5use serde::{Deserialize, Serialize};
6
7pub const TTL_EPHEMERAL: i64 = 3600;
8pub const TTL_SHORT_TERM: i64 = 86_400;
9pub const TTL_LONG_TERM: i64 = 1_209_600;
10
11pub const REL_PRECEDED_BY: &str = "PRECEDED_BY";
15pub const REL_RELATES_TO: &str = "RELATES_TO";
17pub const REL_RELATED: &str = "related";
19pub const REL_SIMILAR_TO: &str = "SIMILAR_TO";
21pub const REL_SHARES_THEME: &str = "SHARES_THEME";
22pub const REL_PARALLEL_CONTEXT: &str = "PARALLEL_CONTEXT";
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum MemoryKind {
26 Episodic,
27 Semantic,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub enum EventType {
38 SessionSummary,
39 TaskCompletion,
40 ErrorPattern,
41 LessonLearned,
42 Decision,
43 BlockedContext,
44 UserPreference,
45 UserFact,
46 AdvisorInsight,
47 GitCommit,
48 GitMerge,
49 GitConflict,
50 SessionStart,
51 SessionEnd,
52 ContextWarning,
53 BudgetAlert,
54 CoordinationSnapshot,
55 Checkpoint,
56 Reminder,
57 Memory,
58 CodeChunk,
59 FileSummary,
60 Unknown(String),
62}
63
64impl EventType {
65 pub fn is_valid(&self) -> bool {
67 !matches!(self, EventType::Unknown(_))
68 }
69
70 pub fn memory_kind(&self) -> MemoryKind {
72 match self {
73 EventType::ErrorPattern
74 | EventType::LessonLearned
75 | EventType::UserPreference
76 | EventType::GitConflict
77 | EventType::Reminder
78 | EventType::Decision => MemoryKind::Semantic,
79 EventType::Unknown(_) => MemoryKind::Episodic,
80 _ => MemoryKind::Episodic,
81 }
82 }
83
84 pub fn default_priority(&self) -> i32 {
86 match self {
87 EventType::ErrorPattern
88 | EventType::LessonLearned
89 | EventType::UserPreference
90 | EventType::GitConflict => 4,
91 EventType::Decision | EventType::TaskCompletion | EventType::AdvisorInsight => 3,
92 EventType::GitCommit
93 | EventType::GitMerge
94 | EventType::SessionEnd
95 | EventType::BudgetAlert => 2,
96 EventType::SessionSummary
97 | EventType::SessionStart
98 | EventType::ContextWarning
99 | EventType::CoordinationSnapshot => 1,
100 EventType::BlockedContext
101 | EventType::Checkpoint
102 | EventType::Reminder
103 | EventType::Memory
104 | EventType::FileSummary => 1,
105 EventType::CodeChunk => 0,
106 EventType::UserFact => 4,
107 EventType::Unknown(_) => 0,
108 }
109 }
110
111 pub fn default_ttl(&self) -> Option<i64> {
113 match self {
114 EventType::SessionSummary => Some(TTL_EPHEMERAL),
115 EventType::TaskCompletion => Some(TTL_LONG_TERM),
116 EventType::ErrorPattern => None,
117 EventType::LessonLearned => None,
118 EventType::Decision => Some(TTL_LONG_TERM),
119 EventType::BlockedContext => Some(TTL_SHORT_TERM),
120 EventType::UserPreference => None,
121 EventType::UserFact => None,
122 EventType::AdvisorInsight => Some(TTL_LONG_TERM),
123 EventType::GitCommit => Some(TTL_LONG_TERM),
124 EventType::GitMerge => Some(TTL_LONG_TERM),
125 EventType::GitConflict => None,
126 EventType::SessionStart => Some(TTL_SHORT_TERM),
127 EventType::SessionEnd => Some(TTL_LONG_TERM),
128 EventType::ContextWarning => Some(TTL_SHORT_TERM),
129 EventType::BudgetAlert => Some(TTL_LONG_TERM),
130 EventType::CoordinationSnapshot => Some(TTL_SHORT_TERM),
131 EventType::Checkpoint => Some(604_800),
132 EventType::Reminder => None,
133 EventType::Memory => Some(TTL_SHORT_TERM),
134 EventType::CodeChunk => Some(TTL_EPHEMERAL),
135 EventType::FileSummary => Some(TTL_SHORT_TERM),
136 EventType::Unknown(_) => Some(TTL_LONG_TERM),
137 }
138 }
139
140 pub fn type_weight(&self) -> f64 {
142 match self {
143 EventType::Checkpoint => 2.5,
144 EventType::Reminder => 3.0,
145 EventType::Decision => 2.0,
146 EventType::LessonLearned => 2.0,
147 EventType::ErrorPattern => 2.0,
148 EventType::UserPreference => 2.0,
149 EventType::TaskCompletion => 1.4,
150 EventType::SessionSummary => 1.2,
151 EventType::BlockedContext => 1.0,
152 EventType::GitCommit => 1.0,
153 EventType::GitMerge => 1.0,
154 EventType::GitConflict => 1.0,
155 EventType::CoordinationSnapshot => 0.2,
156 EventType::Memory => 1.0,
157 _ => 1.0,
158 }
159 }
160
161 pub fn dedup_threshold(&self) -> Option<f64> {
163 match self {
164 EventType::ErrorPattern => Some(0.70),
165 EventType::SessionSummary => Some(0.75),
166 EventType::TaskCompletion => Some(0.85),
167 EventType::Decision => Some(0.80),
168 EventType::LessonLearned => Some(0.85),
169 EventType::UserFact => Some(0.85),
170 EventType::UserPreference => Some(0.75),
171 _ => None,
172 }
173 }
174
175 pub fn is_supersession_type(&self) -> bool {
177 matches!(
178 self,
179 EventType::Decision
180 | EventType::LessonLearned
181 | EventType::UserPreference
182 | EventType::UserFact
183 | EventType::Reminder
184 )
185 }
186
187 pub fn from_optional(s: Option<&str>) -> Option<EventType> {
189 s.map(|v| EventType::from_str(v).unwrap_or_else(|e| match e {}))
190 }
191
192 pub fn types_with_dedup_threshold() -> Vec<EventType> {
194 vec![
196 EventType::ErrorPattern,
197 EventType::SessionSummary,
198 EventType::TaskCompletion,
199 EventType::Decision,
200 EventType::LessonLearned,
201 EventType::UserFact,
202 EventType::UserPreference,
203 ]
204 }
205}
206
207impl fmt::Display for EventType {
208 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209 let s = match self {
210 EventType::SessionSummary => "session_summary",
211 EventType::TaskCompletion => "task_completion",
212 EventType::ErrorPattern => "error_pattern",
213 EventType::LessonLearned => "lesson_learned",
214 EventType::Decision => "decision",
215 EventType::BlockedContext => "blocked_context",
216 EventType::UserPreference => "user_preference",
217 EventType::UserFact => "user_fact",
218 EventType::AdvisorInsight => "advisor_insight",
219 EventType::GitCommit => "git_commit",
220 EventType::GitMerge => "git_merge",
221 EventType::GitConflict => "git_conflict",
222 EventType::SessionStart => "session_start",
223 EventType::SessionEnd => "session_end",
224 EventType::ContextWarning => "context_warning",
225 EventType::BudgetAlert => "budget_alert",
226 EventType::CoordinationSnapshot => "coordination_snapshot",
227 EventType::Checkpoint => "checkpoint",
228 EventType::Reminder => "reminder",
229 EventType::Memory => "memory",
230 EventType::CodeChunk => "code_chunk",
231 EventType::FileSummary => "file_summary",
232 EventType::Unknown(s) => s.as_str(),
233 };
234 write!(f, "{s}")
235 }
236}
237
238impl FromStr for EventType {
239 type Err = std::convert::Infallible;
240
241 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
242 Ok(match s {
243 "session_summary" => EventType::SessionSummary,
244 "task_completion" => EventType::TaskCompletion,
245 "error_pattern" => EventType::ErrorPattern,
246 "lesson_learned" => EventType::LessonLearned,
247 "decision" => EventType::Decision,
248 "blocked_context" => EventType::BlockedContext,
249 "user_preference" => EventType::UserPreference,
250 "user_fact" => EventType::UserFact,
251 "advisor_insight" => EventType::AdvisorInsight,
252 "git_commit" => EventType::GitCommit,
253 "git_merge" => EventType::GitMerge,
254 "git_conflict" => EventType::GitConflict,
255 "session_start" => EventType::SessionStart,
256 "session_end" => EventType::SessionEnd,
257 "context_warning" => EventType::ContextWarning,
258 "budget_alert" => EventType::BudgetAlert,
259 "coordination_snapshot" => EventType::CoordinationSnapshot,
260 "checkpoint" => EventType::Checkpoint,
261 "reminder" => EventType::Reminder,
262 "memory" => EventType::Memory,
263 "code_chunk" => EventType::CodeChunk,
264 "file_summary" => EventType::FileSummary,
265 other => EventType::Unknown(other.to_string()),
266 })
267 }
268}
269
270impl Serialize for EventType {
271 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
272 where
273 S: serde::Serializer,
274 {
275 serializer.serialize_str(&self.to_string())
276 }
277}
278
279impl<'de> Deserialize<'de> for EventType {
280 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
281 where
282 D: serde::Deserializer<'de>,
283 {
284 let s = String::deserialize(deserializer)?;
285 Ok(EventType::from_str(&s).unwrap_or_else(|e| match e {}))
287 }
288}
289
290impl schemars::JsonSchema for EventType {
291 fn schema_name() -> std::borrow::Cow<'static, str> {
292 std::borrow::Cow::Borrowed("EventType")
293 }
294
295 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
296 schemars::json_schema!({
297 "type": "string",
298 "description": "Memory event type",
299 "enum": [
300 "session_summary", "task_completion", "error_pattern", "lesson_learned",
301 "decision", "blocked_context", "user_preference", "user_fact",
302 "advisor_insight", "git_commit", "git_merge", "git_conflict",
303 "session_start", "session_end", "context_warning", "budget_alert",
304 "coordination_snapshot", "checkpoint", "reminder", "memory",
305 "code_chunk", "file_summary"
306 ]
307 })
308 }
309}
310
311#[derive(Debug, Clone)]
312pub struct MemoryInput {
313 pub content: String,
314 pub id: Option<String>,
315 pub tags: Vec<String>,
316 pub importance: f64,
317 pub metadata: serde_json::Value,
318 pub event_type: Option<EventType>,
319 pub session_id: Option<String>,
320 pub project: Option<String>,
321 pub priority: Option<i32>,
322 pub entity_id: Option<String>,
323 pub agent_type: Option<String>,
324 pub ttl_seconds: Option<i64>,
325 pub referenced_date: Option<String>,
328 pub source_type: Option<String>,
330}
331
332impl Default for MemoryInput {
333 fn default() -> Self {
334 Self {
335 content: String::new(),
336 id: None,
337 tags: Vec::new(),
338 importance: 0.5,
339 metadata: serde_json::json!({}),
340 event_type: None,
341 session_id: None,
342 project: None,
343 priority: None,
344 entity_id: None,
345 agent_type: None,
346 ttl_seconds: None,
347 referenced_date: None,
348 source_type: None,
349 }
350 }
351}
352
353impl MemoryInput {
354 pub fn apply_event_type_defaults(&mut self, event_type_str: Option<&str>) {
358 let event_type =
359 EventType::from_optional(event_type_str).or_else(|| self.event_type.clone());
360
361 if self.ttl_seconds.is_none() {
362 self.ttl_seconds = event_type
363 .as_ref()
364 .map(EventType::default_ttl)
365 .unwrap_or(Some(TTL_LONG_TERM));
366 }
367 if self.priority.is_none() {
368 self.priority = event_type.as_ref().map(EventType::default_priority);
369 }
370 self.event_type = event_type;
371 }
372}
373
374#[derive(Debug, Clone, Default)]
375pub struct MemoryUpdate {
376 pub content: Option<String>,
377 pub tags: Option<Vec<String>>,
378 pub importance: Option<f64>,
379 pub metadata: Option<serde_json::Value>,
380 pub event_type: Option<EventType>,
381 pub priority: Option<i32>,
382}
383
384#[derive(Debug, Clone, Default)]
385pub struct SearchOptions {
386 pub event_type: Option<EventType>,
387 pub project: Option<String>,
388 pub session_id: Option<String>,
389 pub include_superseded: Option<bool>,
390 pub importance_min: Option<f64>,
391 pub created_after: Option<String>,
392 pub created_before: Option<String>,
393 pub context_tags: Option<Vec<String>>,
394 pub entity_id: Option<String>,
395 pub agent_type: Option<String>,
396 pub event_after: Option<String>,
398 pub event_before: Option<String>,
400 pub explain: Option<bool>,
402}
403
404#[derive(Debug, Clone, Default)]
405pub struct WelcomeOptions {
406 pub session_id: Option<String>,
407 pub project: Option<String>,
408 pub agent_type: Option<String>,
409 pub entity_id: Option<String>,
410 pub budget_tokens: Option<usize>,
411}
412
413#[derive(Debug, Clone)]
414pub struct CheckpointInput {
415 pub task_title: String,
416 pub progress: String,
417 pub plan: Option<String>,
418 pub files_touched: Option<serde_json::Value>,
419 pub decisions: Option<Vec<String>>,
420 pub key_context: Option<String>,
421 pub next_steps: Option<String>,
422 pub session_id: Option<String>,
423 pub project: Option<String>,
424}
425
426pub fn is_valid_event_type(event_type: &str) -> bool {
429 EventType::from_str(event_type)
430 .map(|et| et.is_valid())
431 .unwrap_or(false)
432}
433
434pub fn parse_duration(text: &str) -> Result<chrono::Duration> {
435 if text.is_empty() {
436 return Err(anyhow::anyhow!("duration cannot be empty"));
437 }
438
439 let mut weeks: i64 = 0;
440 let mut days: i64 = 0;
441 let mut hours: i64 = 0;
442 let mut minutes: i64 = 0;
443 let mut last_rank: i32 = -1;
444 let mut idx: usize = 0;
445 let bytes = text.as_bytes();
446
447 while idx < bytes.len() {
448 if !bytes[idx].is_ascii_digit() {
449 return Err(anyhow::anyhow!("invalid duration format: {text}"));
450 }
451
452 let start = idx;
453 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
454 idx += 1;
455 }
456
457 if idx >= bytes.len() {
458 return Err(anyhow::anyhow!("invalid duration format: {text}"));
459 }
460
461 let value = text[start..idx]
462 .parse::<i64>()
463 .map_err(|_| anyhow::anyhow!("invalid duration value: {text}"))?;
464 let unit = bytes[idx] as char;
465 idx += 1;
466
467 let rank = match unit {
468 'w' => 0,
469 'd' => 1,
470 'h' => 2,
471 'm' => 3,
472 _ => return Err(anyhow::anyhow!("invalid duration unit in: {text}")),
473 };
474
475 if rank <= last_rank {
476 return Err(anyhow::anyhow!("invalid duration order in: {text}"));
477 }
478 last_rank = rank;
479
480 match unit {
481 'w' => weeks = value,
482 'd' => days = value,
483 'h' => hours = value,
484 'm' => minutes = value,
485 _ => return Err(anyhow::anyhow!("invalid duration unit in: {text}")),
486 }
487 }
488
489 let total = chrono::Duration::weeks(weeks)
490 + chrono::Duration::days(days)
491 + chrono::Duration::hours(hours)
492 + chrono::Duration::minutes(minutes);
493
494 if total.num_seconds() <= 0 {
495 return Err(anyhow::anyhow!("duration must be greater than zero"));
496 }
497
498 Ok(total)
499}
500
501#[derive(Debug, Clone, PartialEq, Serialize)]
503pub struct SearchResult {
504 pub id: String,
506 pub content: String,
508 pub tags: Vec<String>,
510 pub importance: f64,
512 pub metadata: serde_json::Value,
514 pub event_type: Option<EventType>,
515 pub session_id: Option<String>,
516 pub project: Option<String>,
517 pub entity_id: Option<String>,
518 pub agent_type: Option<String>,
519}
520
521#[derive(Debug, Clone, PartialEq, Serialize)]
523pub struct SemanticResult {
524 pub id: String,
526 pub content: String,
528 pub tags: Vec<String>,
530 pub importance: f64,
532 pub metadata: serde_json::Value,
534 pub event_type: Option<EventType>,
535 pub session_id: Option<String>,
536 pub project: Option<String>,
537 pub entity_id: Option<String>,
538 pub agent_type: Option<String>,
539 pub score: f32,
541}
542
543#[derive(Debug, Clone)]
544pub struct GraphNode {
545 pub id: String,
546 pub content: String,
547 pub event_type: Option<EventType>,
548 pub metadata: serde_json::Value,
549 pub hop: usize,
550 pub weight: f64,
551 pub edge_type: String,
552 pub created_at: String,
553}
554
555#[derive(Debug, Clone, PartialEq)]
557pub struct Relationship {
558 pub id: String,
560 pub source_id: String,
562 pub target_id: String,
564 pub rel_type: String,
566 pub weight: f64,
567 pub metadata: serde_json::Value,
569 pub created_at: String,
570}
571
572#[derive(Debug, Clone, PartialEq)]
574pub struct ListResult {
575 pub memories: Vec<SearchResult>,
577 pub total: usize,
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize)]
583pub struct BackupInfo {
584 pub path: std::path::PathBuf,
585 pub size_bytes: u64,
586 pub created_at: String,
587}