brainwires_stores/session/mod.rs
1#![deny(missing_docs)]
2//! Pluggable session-persistence for the Brainwires Agent Framework.
3//!
4//! See the crate-level README for an overview. The [`SessionStore`] trait is
5//! the single extension point; [`InMemorySessionStore`] is the default impl
6//! used by tests and ephemeral sessions, and `SqliteSessionStore` (behind the
7//! crate's `sqlite` feature) provides disk-backed persistence.
8
9/// Interactive session-broker surface (live registry, spawn/list/send/history).
10pub mod broker;
11mod error;
12mod in_memory;
13#[cfg(feature = "sqlite")]
14mod sqlite;
15mod types;
16
17use std::sync::Arc;
18
19use anyhow::Result;
20use async_trait::async_trait;
21
22pub use brainwires_core::Message;
23pub use broker::{SessionBroker, SessionMessage, SessionSummary, SpawnRequest, SpawnedSession};
24pub use error::SessionError;
25pub use in_memory::InMemorySessionStore;
26#[cfg(feature = "sqlite")]
27pub use sqlite::SqliteSessionStore;
28pub use types::{SessionId, SessionRecord};
29
30/// Pagination window passed to [`SessionStore::list_paginated`].
31///
32/// `offset` rows are skipped from the start of the listing; `limit`, when
33/// `Some`, caps how many rows are returned. `Default` is `{ offset: 0, limit: None }`,
34/// which is equivalent to the unbounded [`SessionStore::list`] call.
35#[derive(Debug, Clone, Copy, Default)]
36pub struct ListOptions {
37 /// Number of records to skip from the start of the listing.
38 pub offset: usize,
39 /// Maximum number of records to return. `None` means no cap.
40 pub limit: Option<usize>,
41}
42
43impl ListOptions {
44 /// Convenience constructor.
45 pub fn new(offset: usize, limit: Option<usize>) -> Self {
46 Self { offset, limit }
47 }
48}
49
50/// Trait implemented by every session-persistence backend.
51///
52/// Implementations must be cheap to share via `Arc` and safe to call
53/// concurrently from any async context.
54#[async_trait]
55pub trait SessionStore: Send + Sync {
56 /// Load a session's full transcript. Returns `Ok(None)` when the id is
57 /// not known — callers should treat this as "fresh session".
58 async fn load(&self, id: &SessionId) -> Result<Option<Vec<Message>>>;
59
60 /// Overwrite a session's full transcript. Creating the session if
61 /// it didn't already exist.
62 ///
63 /// Implementations should treat the provided slice as the authoritative
64 /// state and persist it atomically — a crash mid-write must leave the
65 /// store with either the old or new transcript, never a partial one.
66 async fn save(&self, id: &SessionId, messages: &[Message]) -> Result<()>;
67
68 /// Enumerate every session the store knows about, newest-last. Returns
69 /// metadata only — use [`Self::load`] to read message content.
70 async fn list(&self) -> Result<Vec<SessionRecord>>;
71
72 /// Enumerate sessions with `offset` / `limit` pagination. The default
73 /// implementation calls [`Self::list`] and slices in memory; backends that
74 /// can push the window down to storage (e.g. SQLite `LIMIT/OFFSET`) should
75 /// override this to avoid loading the full set.
76 async fn list_paginated(&self, opts: ListOptions) -> Result<Vec<SessionRecord>> {
77 let all = self.list().await?;
78 let start = opts.offset.min(all.len());
79 let end = match opts.limit {
80 Some(limit) => start.saturating_add(limit).min(all.len()),
81 None => all.len(),
82 };
83 Ok(all[start..end].to_vec())
84 }
85
86 /// Remove a session. Deleting an unknown id is a no-op (not an error).
87 async fn delete(&self, id: &SessionId) -> Result<()>;
88}
89
90/// Convenience alias used by downstream crates that hold the store behind
91/// an `Arc`.
92pub type ArcSessionStore = Arc<dyn SessionStore>;