Skip to main content

cognee_database/traits/
session_lifecycle_db.rs

1//! `SessionLifecycleDb` trait — repository for the `/api/v1/sessions/*`
2//! HTTP endpoints (E-09, E-10, E-11, E-12).
3//!
4//! Wraps the seven dashboard operations that live in Python's
5//! `cognee/modules/session_lifecycle/metrics.py`:
6//!   * `ensure_and_touch_session` — upsert + bump activity (idempotent)
7//!   * `accumulate_usage` — atomic counter add to session row + per-model
8//!     `session_model_usage` row
9//!   * `get_session_row` — visibility-checked single-row read
10//!   * `list_session_rows` — paginated list with status / since filters
11//!   * `aggregate_stats` — totals / durations / status buckets
12//!   * `cost_by_model` — grouped per-model attribution
13//!
14//! The `effective_status` value for a session is computed at read time —
15//! `running` rows whose `last_activity_at` is older than
16//! `SESSION_ABANDON_AFTER_SECONDS` (default 1800s — Decision 12) report
17//! as `abandoned` *without* mutating the row. This mirrors Python's
18//! `get_effective_status_sql` and keeps abandonment cheap (no sweeper,
19//! no writes on read).
20//!
21//! Conventions match LIB-03's entity layer: UUIDs are persisted as
22//! 32-char hex strings (`uuid_hex.rs`), timestamps as `DateTimeUtc`.
23//! The trait's public Rust signatures take `Uuid` and convert at the
24//! boundary so callers don't see hex strings.
25
26use async_trait::async_trait;
27use chrono::{DateTime, Utc};
28use serde::{Deserialize, Serialize};
29use uuid::Uuid;
30
31use crate::entities::session_record;
32use crate::types::DatabaseError;
33
34/// Filters for `SessionLifecycleDb::list_session_rows`. Field-for-field
35/// parity with Python's `list_session_rows` keyword arguments at
36/// `cognee/modules/session_lifecycle/metrics.py:365-374`.
37#[derive(Debug, Clone)]
38pub struct SessionListFilters {
39    /// Visibility scope: caller's own sessions are always included.
40    pub user_id: Uuid,
41    /// Additional dataset scope — sessions whose `dataset_id` is in this
42    /// list are included via OR'd visibility predicate.
43    pub permitted_dataset_ids: Vec<Uuid>,
44    /// Optional `last_activity_at >= since` filter.
45    pub since: Option<DateTime<Utc>>,
46    /// Optional effective-status filter (`completed` / `failed` /
47    /// `abandoned` / `running`). The repository applies the
48    /// `effective_status` SQL expression so `abandoned` matches running
49    /// rows past the idle threshold.
50    pub status_filter: Option<String>,
51    /// Page size. Caller-validated upstream (E-09 enforces `1..=500`).
52    pub limit: u32,
53    /// Page offset.
54    pub offset: u32,
55    /// Column to sort by. Recognized: `last_activity_at`, `started_at`,
56    /// `ended_at`, `cost_usd`, `tokens_in`, `tokens_out`. Anything else
57    /// silently falls back to `last_activity_at` (mirrors Python's
58    /// `sortable.get(order_by, ...)` lookup at `metrics.py:415-423`).
59    pub order_by: String,
60    /// Direction. `true` → DESC.
61    pub descending: bool,
62}
63
64/// Wraps a stored `session_records` row plus the read-time effective
65/// status (`abandoned` for stale running rows). Mirrors Python's
66/// `SessionRowWithStatus` dataclass at
67/// `cognee/modules/session_lifecycle/metrics.py:336-348`.
68#[derive(Debug, Clone)]
69pub struct SessionRowWithStatus {
70    pub record: session_record::Model,
71    pub effective_status: String,
72}
73
74impl SessionRowWithStatus {
75    /// Render to a JSON object whose key order matches Python's
76    /// `to_dict()` — entity dict + `effective_status`.
77    pub fn to_dict(&self) -> serde_json::Value {
78        let mut value = self.record.to_dict();
79        if let Some(map) = value.as_object_mut() {
80            map.insert(
81                "effective_status".to_string(),
82                serde_json::Value::String(self.effective_status.clone()),
83            );
84        }
85        value
86    }
87}
88
89/// Paginated envelope returned by `list_session_rows`. Parity with
90/// Python's `SessionListPage` dataclass at `metrics.py:351-362`.
91#[derive(Debug, Clone)]
92pub struct SessionListPage {
93    pub sessions: Vec<SessionRowWithStatus>,
94    pub total: i64,
95    pub limit: u32,
96    pub offset: u32,
97}
98
99impl SessionListPage {
100    /// `true` when pagination has more rows beyond the current page.
101    /// Matches Python's `has_more` property at `metrics.py:360-362`.
102    pub fn has_more(&self) -> bool {
103        let returned = i64::try_from(self.sessions.len()).unwrap_or(i64::MAX);
104        let offset = i64::from(self.offset);
105        offset.saturating_add(returned) < self.total
106    }
107}
108
109/// Aggregate counters for `GET /api/v1/sessions/stats`. Field-for-field
110/// parity with the Python response body at
111/// `cognee/api/v1/sessions/routers/get_sessions_router.py:179-196`.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct SessionStats {
114    pub sessions: i64,
115    pub total_spend_usd: f64,
116    pub avg_spend_per_session_usd: f64,
117    pub tokens_in: i64,
118    pub tokens_out: i64,
119    pub tokens_total: i64,
120    pub agent_time_s: f64,
121    pub avg_session_s: f64,
122    pub success_rate: f64,
123    pub completed: i64,
124    pub failed: i64,
125    pub abandoned: i64,
126    pub running: i64,
127}
128
129/// Single per-model row for `GET /api/v1/sessions/cost-by-model`.
130/// Parity with the Python response items at
131/// `cognee/api/v1/sessions/routers/get_sessions_router.py:241-251`.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct CostByModelRow {
134    pub model: String,
135    pub session_count: i64,
136    pub cost_usd: f64,
137    pub tokens_in: i64,
138    pub tokens_out: i64,
139}
140
141/// Repository trait for `/api/v1/sessions/*`. See module docs.
142#[async_trait]
143#[allow(clippy::too_many_arguments)] // accumulate_usage mirrors Python kw-args at metrics.py:133-141
144pub trait SessionLifecycleDb: Send + Sync {
145    /// Upsert a session row, bumping `last_activity_at` if the row is
146    /// already running. Mirrors `metrics.py:62-130`.
147    async fn ensure_and_touch_session(
148        &self,
149        session_id: &str,
150        user_id: Uuid,
151        dataset_id: Option<Uuid>,
152    ) -> Result<(), DatabaseError>;
153
154    /// Atomically add usage counters to the session row + per-model
155    /// row. Mirrors `metrics.py:133-241`.
156    async fn accumulate_usage(
157        &self,
158        session_id: &str,
159        user_id: Uuid,
160        model: Option<&str>,
161        tokens_in: i64,
162        tokens_out: i64,
163        cost_usd: f64,
164        errored: bool,
165    ) -> Result<(), DatabaseError>;
166
167    /// Visibility-checked single-row read. Mirrors `metrics.py:295-333`.
168    async fn get_session_row(
169        &self,
170        session_id: &str,
171        user_id: Uuid,
172        permitted_dataset_ids: &[Uuid],
173        prefer_other_owner: bool,
174    ) -> Result<Option<SessionRowWithStatus>, DatabaseError>;
175
176    /// Paginated list with `effective_status` filter support. Mirrors
177    /// `metrics.py:365-438`.
178    async fn list_session_rows(
179        &self,
180        filters: SessionListFilters,
181    ) -> Result<SessionListPage, DatabaseError>;
182
183    /// Dashboard counters for `GET /sessions/stats`. Mirrors
184    /// `get_sessions_router.py:112-196`.
185    async fn aggregate_stats(
186        &self,
187        user_id: Uuid,
188        permitted_dataset_ids: &[Uuid],
189        since: Option<DateTime<Utc>>,
190    ) -> Result<SessionStats, DatabaseError>;
191
192    /// Per-model attribution for `GET /sessions/cost-by-model`.
193    /// Mirrors `get_sessions_router.py:198-252`.
194    async fn cost_by_model(
195        &self,
196        user_id: Uuid,
197        permitted_dataset_ids: &[Uuid],
198        since: Option<DateTime<Utc>>,
199    ) -> Result<Vec<CostByModelRow>, DatabaseError>;
200}