cel_memory/session.rs
1//! Memory session — a coherent unit of work.
2//!
3//! A chat conversation or a delegated job. Memory chunks tie back to a session
4//! via [`MemoryChunk::session_id`]. The summarizer runs at end-of-session and
5//! produces a `JobSummary` chunk per session.
6//!
7//! [`MemoryChunk::session_id`]: crate::MemoryChunk::session_id
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13/// A session record.
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub struct MemorySession {
16 /// uuid v7.
17 pub id: String,
18 /// When the session opened.
19 pub started_at: DateTime<Utc>,
20 /// When the session closed, if it has.
21 pub ended_at: Option<DateTime<Utc>>,
22 /// Normalised caller — `"embedded"`, `"mcp:cursor"`, etc.
23 pub caller_id: String,
24 /// Human-readable title (agent- or user-set).
25 pub title: Option<String>,
26 /// End-of-session synthesis. `None` until `close_session` has run and
27 /// summarization (where available) has produced one.
28 pub summary: Option<String>,
29 /// Outcome of the session.
30 pub outcome: SessionOutcome,
31 /// Free-form metadata.
32 #[serde(default)]
33 pub metadata: Value,
34}
35
36/// Input to [`crate::MemoryProvider::open_session`].
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
38pub struct NewMemorySession {
39 /// Caller opening the session.
40 pub caller_id: String,
41 /// Optional title.
42 #[serde(default)]
43 pub title: Option<String>,
44 /// Optional metadata.
45 #[serde(default)]
46 pub metadata: Value,
47}
48
49/// Outcome states for a session.
50#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
51#[serde(rename_all = "snake_case")]
52pub enum SessionOutcome {
53 /// Still in progress. The session has been opened but not closed.
54 Open,
55 /// The session completed and the agent considers the goal achieved.
56 Success,
57 /// The session completed but the goal was not achieved.
58 Failure,
59 /// The session was aborted (user closed the window mid-job and didn't
60 /// resume, agent self-terminated, etc.).
61 Aborted,
62}
63
64/// Filter for [`crate::MemoryProvider::list_sessions`].
65#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
66pub struct SessionFilter {
67 /// Restrict to sessions opened by a specific caller.
68 #[serde(default)]
69 pub caller_id: Option<String>,
70 /// Restrict to sessions with a specific outcome.
71 #[serde(default)]
72 pub outcome: Option<SessionOutcome>,
73 /// Lower bound on `started_at`.
74 #[serde(default)]
75 pub since: Option<DateTime<Utc>>,
76 /// Upper bound on `started_at`.
77 #[serde(default)]
78 pub until: Option<DateTime<Utc>>,
79 /// If true, only return sessions still `Open`.
80 #[serde(default)]
81 pub open_only: bool,
82 /// Maximum number of sessions to return. `None` = no limit.
83 #[serde(default)]
84 pub limit: Option<usize>,
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use serde_json::json;
91
92 #[test]
93 fn outcome_serializes_snake_case() {
94 assert_eq!(
95 serde_json::to_value(SessionOutcome::Open).unwrap(),
96 json!("open")
97 );
98 assert_eq!(
99 serde_json::to_value(SessionOutcome::Aborted).unwrap(),
100 json!("aborted")
101 );
102 }
103
104 #[test]
105 fn filter_defaults() {
106 let f = SessionFilter::default();
107 assert!(f.caller_id.is_none());
108 assert!(f.outcome.is_none());
109 assert!(!f.open_only);
110 assert!(f.limit.is_none());
111 }
112}