use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde_json::Value;
use super::error::TracingError;
use super::models::{
EventReward, MarkovBlanketMessage, OutcomeReward, SessionTimeStep, SessionTrace, TracingEvent,
};
#[async_trait]
pub trait TraceStorage: Send + Sync {
async fn initialize(&self) -> Result<(), TracingError>;
async fn close(&self) -> Result<(), TracingError>;
async fn ensure_session(
&self,
session_id: &str,
created_at: DateTime<Utc>,
metadata: &Value,
) -> Result<(), TracingError>;
async fn get_session(&self, session_id: &str) -> Result<Option<SessionTrace>, TracingError>;
async fn delete_session(&self, session_id: &str) -> Result<bool, TracingError>;
async fn ensure_timestep(
&self,
session_id: &str,
step: &SessionTimeStep,
) -> Result<i64, TracingError>;
async fn update_timestep(
&self,
session_id: &str,
step_id: &str,
completed_at: Option<DateTime<Utc>>,
) -> Result<(), TracingError>;
async fn insert_event(
&self,
session_id: &str,
timestep_db_id: Option<i64>,
event: &TracingEvent,
) -> Result<i64, TracingError>;
async fn insert_message(
&self,
session_id: &str,
timestep_db_id: Option<i64>,
msg: &MarkovBlanketMessage,
) -> Result<i64, TracingError>;
async fn insert_outcome_reward(
&self,
session_id: &str,
reward: &OutcomeReward,
) -> Result<i64, TracingError>;
async fn insert_event_reward(
&self,
session_id: &str,
event_id: i64,
message_id: Option<i64>,
turn_number: Option<i32>,
reward: &EventReward,
) -> Result<i64, TracingError>;
async fn query(&self, sql: &str, params: QueryParams) -> Result<Vec<Value>, TracingError>;
async fn update_session_counts(&self, session_id: &str) -> Result<(), TracingError>;
}
#[derive(Debug, Clone, Default)]
pub enum QueryParams {
#[default]
None,
Positional(Vec<Value>),
Named(Vec<(String, Value)>),
}
impl QueryParams {
pub fn is_empty(&self) -> bool {
matches!(self, QueryParams::None)
}
}
impl From<Vec<Value>> for QueryParams {
fn from(values: Vec<Value>) -> Self {
if values.is_empty() {
QueryParams::None
} else {
QueryParams::Positional(values)
}
}
}
impl From<Vec<(String, Value)>> for QueryParams {
fn from(values: Vec<(String, Value)>) -> Self {
if values.is_empty() {
QueryParams::None
} else {
QueryParams::Named(values)
}
}
}
#[derive(Debug, Clone)]
pub struct StorageConfig {
pub db_url: String,
pub auth_token: Option<String>,
pub auto_init: bool,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
db_url: ":memory:".to_string(),
auth_token: None,
auto_init: true,
}
}
}
impl StorageConfig {
pub fn memory() -> Self {
Self::default()
}
pub fn file(path: impl Into<String>) -> Self {
Self {
db_url: path.into(),
auth_token: None,
auto_init: true,
}
}
pub fn turso(url: impl Into<String>, token: impl Into<String>) -> Self {
Self {
db_url: url.into(),
auth_token: Some(token.into()),
auto_init: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_storage_config_default() {
let config = StorageConfig::default();
assert_eq!(config.db_url, ":memory:");
assert!(config.auth_token.is_none());
assert!(config.auto_init);
}
#[test]
fn test_storage_config_file() {
let config = StorageConfig::file("/tmp/test.db");
assert_eq!(config.db_url, "/tmp/test.db");
}
#[test]
fn test_storage_config_turso() {
let config = StorageConfig::turso("libsql://test.turso.io", "token123");
assert_eq!(config.db_url, "libsql://test.turso.io");
assert_eq!(config.auth_token, Some("token123".to_string()));
}
}