1use std::sync::Arc;
4
5use ag_agent::SessionStats;
6use async_trait::async_trait;
7use sqlx::SqlitePool;
8
9use crate::DbError;
10use crate::timestamp::TimestampSource;
11
12pub struct SessionUsageRow {
15 pub created_at: i64,
17 pub input_tokens: i64,
19 pub invocation_count: i64,
21 pub model: String,
23 pub output_tokens: i64,
25 pub session_id: Option<String>,
27}
28
29#[cfg_attr(test, mockall::automock)]
31#[async_trait]
32pub trait UsageRepository: Send + Sync {
33 async fn load_session_usage(&self, session_id: &str) -> Result<Vec<SessionUsageRow>, DbError>;
35
36 async fn upsert_session_usage(
38 &self,
39 session_id: &str,
40 model: &str,
41 stats: &SessionStats,
42 ) -> Result<(), DbError>;
43}
44
45#[derive(Clone)]
47pub(crate) struct SqliteUsageRepository {
48 pool: SqlitePool,
49 timestamp_source: Arc<dyn TimestampSource>,
50}
51
52impl SqliteUsageRepository {
53 pub(crate) fn new(pool: SqlitePool, timestamp_source: Arc<dyn TimestampSource>) -> Self {
56 Self {
57 pool,
58 timestamp_source,
59 }
60 }
61
62 fn now(&self) -> i64 {
63 self.timestamp_source.now_timestamp_seconds()
64 }
65}
66
67#[async_trait]
68impl UsageRepository for SqliteUsageRepository {
69 async fn load_session_usage(&self, session_id: &str) -> Result<Vec<SessionUsageRow>, DbError> {
70 let rows = sqlx::query_as!(
71 SessionUsageRow,
72 r#"
73SELECT session_id, model, created_at, input_tokens, invocation_count, output_tokens
74FROM session_usage
75WHERE session_id = ?
76ORDER BY model
77 "#,
78 session_id
79 )
80 .fetch_all(&self.pool)
81 .await?;
82
83 Ok(rows)
84 }
85
86 async fn upsert_session_usage(
87 &self,
88 session_id: &str,
89 model: &str,
90 stats: &SessionStats,
91 ) -> Result<(), DbError> {
92 if stats.input_tokens == 0 && stats.output_tokens == 0 {
93 return Ok(());
94 }
95
96 let now = self.now();
97
98 sqlx::query!(
99 r"
100INSERT INTO session_usage (
101 session_id, model, created_at, input_tokens, output_tokens, invocation_count
102)
103VALUES (?, ?, ?, ?, ?, 1)
104ON CONFLICT(session_id, model) DO UPDATE SET
105 input_tokens = input_tokens + excluded.input_tokens,
106 output_tokens = output_tokens + excluded.output_tokens,
107 invocation_count = invocation_count + 1
108",
109 session_id,
110 model,
111 now,
112 stats.input_tokens.cast_signed(),
113 stats.output_tokens.cast_signed()
114 )
115 .execute(&self.pool)
116 .await?;
117
118 Ok(())
119 }
120}