1use rusqlite::{params, Connection};
7
8use crate::agent_loop::AgentLoopLog;
9
10#[derive(Debug, thiserror::Error)]
12pub enum HistoryError {
13 #[error("Database error: {0}")]
14 Db(#[from] rusqlite::Error),
15}
16
17pub struct AgentHistoryStore {
19 conn: Connection,
20}
21
22impl AgentHistoryStore {
23 pub fn new(db_path: &str) -> Result<Self, HistoryError> {
27 let conn = Connection::open(db_path)?;
28
29 conn.execute_batch(
30 "CREATE TABLE IF NOT EXISTS agent_loop_history (
31 id INTEGER PRIMARY KEY AUTOINCREMENT,
32 agent_id TEXT NOT NULL,
33 iteration INTEGER NOT NULL,
34 timestamp TEXT NOT NULL,
35 phase TEXT NOT NULL,
36 success INTEGER NOT NULL,
37 message TEXT NOT NULL,
38 decision_summary TEXT,
39 tool_turn_count INTEGER NOT NULL,
40 risk_blocked INTEGER NOT NULL,
41 order_executed INTEGER NOT NULL
42 );
43 CREATE INDEX IF NOT EXISTS idx_agent_history_agent_id
44 ON agent_loop_history(agent_id, id DESC);",
45 )?;
46
47 Ok(Self { conn })
48 }
49
50 pub fn insert(&self, log: &AgentLoopLog) -> Result<(), HistoryError> {
52 self.conn.execute(
53 "INSERT INTO agent_loop_history
54 (agent_id, iteration, timestamp, phase, success, message,
55 decision_summary, tool_turn_count, risk_blocked, order_executed)
56 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
57 params![
58 log.agent_id,
59 log.iteration,
60 log.timestamp,
61 log.phase,
62 log.success as i32,
63 log.message,
64 log.decision_summary,
65 log.tool_turn_count,
66 log.risk_blocked as i32,
67 log.order_executed as i32,
68 ],
69 )?;
70 Ok(())
71 }
72
73 pub fn list(&self, agent_id: &str, limit: usize) -> Result<Vec<AgentLoopLog>, HistoryError> {
75 let mut stmt = self.conn.prepare(
76 "SELECT agent_id, iteration, timestamp, phase, success, message,
77 decision_summary, tool_turn_count, risk_blocked, order_executed
78 FROM agent_loop_history
79 WHERE agent_id = ?1
80 ORDER BY id DESC
81 LIMIT ?2",
82 )?;
83
84 let rows = stmt.query_map(params![agent_id, limit as i64], |row| {
85 Ok(AgentLoopLog {
86 agent_id: row.get(0)?,
87 iteration: row.get(1)?,
88 timestamp: row.get(2)?,
89 phase: row.get(3)?,
90 success: row.get::<_, i32>(4)? != 0,
91 message: row.get(5)?,
92 decision_summary: row.get(6)?,
93 tool_turn_count: row.get::<_, i32>(7)? as u32,
94 risk_blocked: row.get::<_, i32>(8)? != 0,
95 order_executed: row.get::<_, i32>(9)? != 0,
96 })
97 })?;
98
99 let mut logs = Vec::new();
100 for row in rows {
101 logs.push(row?);
102 }
103 Ok(logs)
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 fn sample_log(agent_id: &str, iteration: u64) -> AgentLoopLog {
112 AgentLoopLog {
113 agent_id: agent_id.to_string(),
114 iteration,
115 timestamp: "2026-03-22T10:00:00Z".to_string(),
116 phase: "complete".to_string(),
117 success: true,
118 message: "No action".to_string(),
119 decision_summary: Some("Hold".to_string()),
120 tool_turn_count: 1,
121 risk_blocked: false,
122 order_executed: false,
123 }
124 }
125
126 #[test]
127 fn insert_and_list() {
128 let store = AgentHistoryStore::new(":memory:").unwrap();
129 store.insert(&sample_log("agent-1", 0)).unwrap();
130 store.insert(&sample_log("agent-1", 1)).unwrap();
131 store.insert(&sample_log("agent-2", 0)).unwrap();
132
133 let logs = store.list("agent-1", 10).unwrap();
134 assert_eq!(logs.len(), 2);
135 assert_eq!(logs[0].iteration, 1);
137 assert_eq!(logs[1].iteration, 0);
138
139 let logs2 = store.list("agent-2", 10).unwrap();
140 assert_eq!(logs2.len(), 1);
141 }
142
143 #[test]
144 fn list_respects_limit() {
145 let store = AgentHistoryStore::new(":memory:").unwrap();
146 for i in 0..10 {
147 store.insert(&sample_log("agent-1", i)).unwrap();
148 }
149 let logs = store.list("agent-1", 3).unwrap();
150 assert_eq!(logs.len(), 3);
151 assert_eq!(logs[0].iteration, 9);
152 }
153
154 #[test]
155 fn empty_list() {
156 let store = AgentHistoryStore::new(":memory:").unwrap();
157 let logs = store.list("nonexistent", 10).unwrap();
158 assert!(logs.is_empty());
159 }
160}