use rusqlite::Connection;
use crate::error::Result;
pub struct Store {
conn: Connection,
}
#[derive(Debug, Clone, PartialEq)]
pub struct StepRecord {
pub step: u32,
pub decision: String,
pub result: String,
pub prompt: String,
pub tool_call: String,
pub tokens: u64,
}
impl StepRecord {
pub fn new(step: u32, decision: impl Into<String>, result: impl Into<String>) -> Self {
Self {
step,
decision: decision.into(),
result: result.into(),
prompt: String::new(),
tool_call: String::new(),
tokens: 0,
}
}
pub fn with_trace(
mut self,
prompt: impl Into<String>,
tool_call: impl Into<String>,
tokens: u64,
) -> Self {
self.prompt = prompt.into();
self.tool_call = tool_call.into();
self.tokens = tokens;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PolicyEvent {
pub step: u32,
pub kind: String,
pub act: String,
pub target: String,
pub rule: Option<String>,
pub layer: Option<String>,
pub decision: Option<String>,
pub source: Option<String>,
pub performed: Option<String>,
}
impl PolicyEvent {
pub fn refusal(step: u32, act: impl Into<String>, target: impl Into<String>) -> Self {
Self {
step,
kind: "refusal".into(),
act: act.into(),
target: target.into(),
rule: None,
layer: None,
decision: None,
source: None,
performed: None,
}
}
pub fn decision(
step: u32,
act: impl Into<String>,
target: impl Into<String>,
decision: impl Into<String>,
source: impl Into<String>,
) -> Self {
Self {
kind: "decision".into(),
decision: Some(decision.into()),
source: Some(source.into()),
..Self::refusal(step, act, target)
}
}
pub fn with_rule(mut self, rule: impl Into<String>, layer: impl Into<String>) -> Self {
self.rule = Some(rule.into());
self.layer = Some(layer.into());
self
}
pub fn with_performed(mut self, performed: impl Into<String>) -> Self {
self.performed = Some(performed.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pending {
pub id: i64,
pub run_id: i64,
pub step: u32,
pub act: String,
pub target: String,
pub content: Option<String>,
pub resolved: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentEvent {
pub run_id: i64,
pub step: u32,
pub kind: String,
pub child_run_id: Option<i64>,
pub detail: Option<String>,
pub tokens: Option<u64>,
pub remaining: Option<u64>,
}
impl AgentEvent {
pub fn spawn(run_id: i64, step: u32, child_run_id: i64, goal: impl Into<String>) -> Self {
Self {
run_id,
step,
kind: "spawn".into(),
child_run_id: Some(child_run_id),
detail: Some(goal.into()),
tokens: None,
remaining: None,
}
}
pub fn spawn_refused(run_id: i64, step: u32, cap: &str) -> Self {
Self {
run_id,
step,
kind: "spawn_refused".into(),
child_run_id: None,
detail: Some(cap.into()),
tokens: None,
remaining: None,
}
}
pub fn budget_draw(run_id: i64, step: u32, tokens: u64, remaining: u64) -> Self {
Self {
run_id,
step,
kind: "budget_draw".into(),
child_run_id: None,
detail: None,
tokens: Some(tokens),
remaining: Some(remaining),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SandboxEvent {
pub run_id: i64,
pub step: u32,
pub kind: String,
pub backend: Option<String>,
pub detail: Option<String>,
}
impl SandboxEvent {
pub fn create(run_id: i64, step: u32, backend: &str) -> Self {
Self { run_id, step, kind: "create".into(), backend: Some(backend.into()), detail: None }
}
pub fn exec(run_id: i64, step: u32, backend: &str, argv: &str) -> Self {
Self {
run_id,
step,
kind: "exec".into(),
backend: Some(backend.into()),
detail: Some(argv.into()),
}
}
pub fn cap_hit(run_id: i64, step: u32, cap: &str) -> Self {
Self { run_id, step, kind: "cap_hit".into(), backend: None, detail: Some(cap.into()) }
}
pub fn destroy(run_id: i64, step: u32) -> Self {
Self { run_id, step, kind: "destroy".into(), backend: None, detail: None }
}
}
impl Store {
pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self> {
Self::from_conn(Connection::open(path)?)
}
pub fn memory() -> Result<Self> {
Self::from_conn(Connection::open_in_memory()?)
}
fn from_conn(conn: Connection) -> Result<Self> {
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
goal TEXT NOT NULL,
file TEXT NOT NULL,
outcome TEXT,
provider TEXT
);
CREATE TABLE IF NOT EXISTS steps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER NOT NULL REFERENCES runs(id),
step INTEGER NOT NULL,
decision TEXT NOT NULL,
result TEXT NOT NULL,
prompt TEXT NOT NULL DEFAULT '',
tool_call TEXT NOT NULL DEFAULT '',
tokens INTEGER NOT NULL DEFAULT 0
);",
)?;
for col in [
"prompt TEXT NOT NULL DEFAULT ''",
"tool_call TEXT NOT NULL DEFAULT ''",
"tokens INTEGER NOT NULL DEFAULT 0",
] {
let _ = conn.execute(&format!("ALTER TABLE steps ADD COLUMN {col}"), []);
}
let _ = conn.execute("ALTER TABLE runs ADD COLUMN provider TEXT", []);
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS policy_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER NOT NULL,
step INTEGER NOT NULL,
kind TEXT NOT NULL,
act TEXT NOT NULL,
target TEXT NOT NULL,
rule TEXT,
layer TEXT,
decision TEXT,
source TEXT,
performed TEXT
);
CREATE TABLE IF NOT EXISTS pending_approvals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER NOT NULL,
step INTEGER NOT NULL,
act TEXT NOT NULL,
target TEXT NOT NULL,
content TEXT,
resolved TEXT
);",
)?;
let _ = conn.execute("ALTER TABLE runs ADD COLUMN parent_run_id INTEGER", []);
let _ = conn.execute("ALTER TABLE runs ADD COLUMN depth INTEGER NOT NULL DEFAULT 0", []);
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS agent_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER NOT NULL,
step INTEGER NOT NULL,
kind TEXT NOT NULL,
child_run_id INTEGER,
detail TEXT,
tokens INTEGER,
remaining INTEGER
);",
)?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS sandbox_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER NOT NULL,
step INTEGER NOT NULL,
kind TEXT NOT NULL,
backend TEXT,
detail TEXT
);",
)?;
Ok(Self { conn })
}
pub fn record_event(&self, run_id: i64, e: &PolicyEvent) -> Result<()> {
self.conn.execute(
"INSERT INTO policy_events
(run_id, step, kind, act, target, rule, layer, decision, source, performed)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
(
run_id,
e.step,
&e.kind,
&e.act,
&e.target,
&e.rule,
&e.layer,
&e.decision,
&e.source,
&e.performed,
),
)?;
Ok(())
}
pub fn events(&self, run_id: i64) -> Result<Vec<PolicyEvent>> {
let mut stmt = self.conn.prepare(
"SELECT step, kind, act, target, rule, layer, decision, source, performed
FROM policy_events WHERE run_id = ?1 ORDER BY id ASC",
)?;
let rows = stmt.query_map([run_id], |r| {
Ok(PolicyEvent {
step: r.get::<_, i64>(0)? as u32,
kind: r.get(1)?,
act: r.get(2)?,
target: r.get(3)?,
rule: r.get(4)?,
layer: r.get(5)?,
decision: r.get(6)?,
source: r.get(7)?,
performed: r.get(8)?,
})
})?;
Ok(rows.collect::<std::result::Result<_, _>>()?)
}
pub fn put_pending(
&self,
run_id: i64,
step: u32,
act: &str,
target: &str,
content: Option<&str>,
) -> Result<i64> {
self.conn.execute(
"INSERT INTO pending_approvals (run_id, step, act, target, content)
VALUES (?1, ?2, ?3, ?4, ?5)",
(run_id, step, act, target, content),
)?;
Ok(self.conn.last_insert_rowid())
}
pub fn pending(&self, request_id: i64) -> Result<Option<Pending>> {
let mut stmt = self.conn.prepare(
"SELECT id, run_id, step, act, target, content, resolved
FROM pending_approvals WHERE id = ?1",
)?;
let mut rows = stmt.query_map([request_id], |r| {
Ok(Pending {
id: r.get(0)?,
run_id: r.get(1)?,
step: r.get::<_, i64>(2)? as u32,
act: r.get(3)?,
target: r.get(4)?,
content: r.get(5)?,
resolved: r.get(6)?,
})
})?;
Ok(rows.next().transpose()?)
}
pub fn resolve_pending(&self, request_id: i64, decision: &str) -> Result<()> {
self.conn.execute(
"UPDATE pending_approvals SET resolved = ?1 WHERE id = ?2",
(decision, request_id),
)?;
Ok(())
}
pub fn start_run(&self, goal: &str, file: &str) -> Result<i64> {
self.conn
.execute("INSERT INTO runs (goal, file) VALUES (?1, ?2)", (goal, file))?;
Ok(self.conn.last_insert_rowid())
}
pub fn start_child_run(
&self,
goal: &str,
file: &str,
parent_run_id: i64,
depth: u32,
) -> Result<i64> {
self.conn.execute(
"INSERT INTO runs (goal, file, parent_run_id, depth) VALUES (?1, ?2, ?3, ?4)",
(goal, file, parent_run_id, depth),
)?;
Ok(self.conn.last_insert_rowid())
}
pub fn record_agent_event(&self, e: &AgentEvent) -> Result<()> {
self.conn.execute(
"INSERT INTO agent_events (run_id, step, kind, child_run_id, detail, tokens, remaining)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
(
e.run_id,
e.step,
&e.kind,
e.child_run_id,
&e.detail,
e.tokens,
e.remaining,
),
)?;
Ok(())
}
pub fn agent_events(&self, run_id: i64) -> Result<Vec<AgentEvent>> {
let mut stmt = self.conn.prepare(
"SELECT run_id, step, kind, child_run_id, detail, tokens, remaining
FROM agent_events WHERE run_id = ?1 ORDER BY id ASC",
)?;
let rows = stmt.query_map([run_id], |r| {
Ok(AgentEvent {
run_id: r.get(0)?,
step: r.get::<_, i64>(1)? as u32,
kind: r.get(2)?,
child_run_id: r.get(3)?,
detail: r.get(4)?,
tokens: r.get::<_, Option<i64>>(5)?.map(|n| n as u64),
remaining: r.get::<_, Option<i64>>(6)?.map(|n| n as u64),
})
})?;
Ok(rows.collect::<std::result::Result<_, _>>()?)
}
pub fn record_sandbox_event(&self, e: &SandboxEvent) -> Result<()> {
self.conn.execute(
"INSERT INTO sandbox_events (run_id, step, kind, backend, detail)
VALUES (?1, ?2, ?3, ?4, ?5)",
(e.run_id, e.step, &e.kind, &e.backend, &e.detail),
)?;
Ok(())
}
pub fn sandbox_events(&self, run_id: i64) -> Result<Vec<SandboxEvent>> {
let mut stmt = self.conn.prepare(
"SELECT run_id, step, kind, backend, detail
FROM sandbox_events WHERE run_id = ?1 ORDER BY id ASC",
)?;
let rows = stmt.query_map([run_id], |r| {
Ok(SandboxEvent {
run_id: r.get(0)?,
step: r.get::<_, i64>(1)? as u32,
kind: r.get(2)?,
backend: r.get(3)?,
detail: r.get(4)?,
})
})?;
Ok(rows.collect::<std::result::Result<_, _>>()?)
}
pub fn children(&self, run_id: i64) -> Result<Vec<i64>> {
let mut stmt = self
.conn
.prepare("SELECT id FROM runs WHERE parent_run_id = ?1 ORDER BY id ASC")?;
let rows = stmt.query_map([run_id], |r| r.get(0))?;
Ok(rows.collect::<std::result::Result<_, _>>()?)
}
pub fn parent(&self, run_id: i64) -> Result<Option<i64>> {
Ok(self.conn.query_row(
"SELECT parent_run_id FROM runs WHERE id = ?1",
[run_id],
|r| r.get(0),
)?)
}
pub fn depth(&self, run_id: i64) -> Result<u32> {
let d: i64 = self
.conn
.query_row("SELECT depth FROM runs WHERE id = ?1", [run_id], |r| r.get(0))?;
Ok(d as u32)
}
pub fn record(&self, run_id: i64, step: &StepRecord) -> Result<()> {
self.conn.execute(
"INSERT INTO steps (run_id, step, decision, result, prompt, tool_call, tokens)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
(
run_id,
step.step,
&step.decision,
&step.result,
&step.prompt,
&step.tool_call,
step.tokens,
),
)?;
Ok(())
}
pub fn set_provider(&self, run_id: i64, provider: &str) -> Result<()> {
self.conn.execute(
"UPDATE runs SET provider = ?1 WHERE id = ?2",
(provider, run_id),
)?;
Ok(())
}
pub fn provider(&self, run_id: i64) -> Result<Option<String>> {
Ok(self
.conn
.query_row("SELECT provider FROM runs WHERE id = ?1", [run_id], |r| {
r.get(0)
})?)
}
pub fn finish_run(&self, run_id: i64, outcome: &str) -> Result<()> {
self.conn
.execute("UPDATE runs SET outcome = ?1 WHERE id = ?2", (outcome, run_id))?;
Ok(())
}
pub fn last_step(&self, run_id: i64) -> Result<u32> {
let n: i64 = self.conn.query_row(
"SELECT COALESCE(MAX(step), 0) FROM steps WHERE run_id = ?1",
[run_id],
|r| r.get(0),
)?;
Ok(n as u32)
}
pub fn steps(&self, run_id: i64) -> Result<Vec<StepRecord>> {
let mut stmt = self.conn.prepare(
"SELECT step, decision, result, prompt, tool_call, tokens
FROM steps WHERE run_id = ?1 ORDER BY step ASC, id ASC",
)?;
let rows = stmt.query_map([run_id], |r| {
Ok(StepRecord {
step: r.get::<_, i64>(0)? as u32,
decision: r.get(1)?,
result: r.get(2)?,
prompt: r.get(3)?,
tool_call: r.get(4)?,
tokens: r.get::<_, i64>(5)? as u64,
})
})?;
Ok(rows.collect::<std::result::Result<_, _>>()?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn refusals_record_action_target_rule_and_layer() {
let store = Store::memory().unwrap();
let run = store.start_run("goal", "root").unwrap();
store
.record_event(
run,
&PolicyEvent::refusal(2, "write", "secrets/key.txt")
.with_rule("secrets/*", "base"),
)
.unwrap();
let events = store.events(run).unwrap();
assert_eq!(events.len(), 1);
let e = &events[0];
assert_eq!(e.kind, "refusal");
assert_eq!(e.act, "write");
assert_eq!(e.target, "secrets/key.txt");
assert_eq!(e.rule.as_deref(), Some("secrets/*"));
assert_eq!(e.layer.as_deref(), Some("base"));
}
#[test]
fn decisions_record_their_value_source_and_any_altered_target() {
let store = Store::memory().unwrap();
let run = store.start_run("goal", "root").unwrap();
store
.record_event(
run,
&PolicyEvent::decision(1, "write", "src/a.rs", "approve", "stdin")
.with_performed("src/sandbox/a.rs"),
)
.unwrap();
store
.record_event(
run,
&PolicyEvent::decision(2, "write", "src/b.rs", "approve", "remembered"),
)
.unwrap();
let events = store.events(run).unwrap();
assert_eq!(events.len(), 2);
assert_eq!(events[0].decision.as_deref(), Some("approve"));
assert_eq!(events[0].target, "src/a.rs");
assert_eq!(events[0].performed.as_deref(), Some("src/sandbox/a.rs"));
assert_eq!(events[1].source.as_deref(), Some("remembered"));
assert_eq!(events[1].performed, None);
}
#[test]
fn a_pre_0_4_database_migrates_in_place_and_keeps_its_rows() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("runs.db");
{
let conn = rusqlite::Connection::open(&path).unwrap();
conn.execute_batch(
"CREATE TABLE runs (id INTEGER PRIMARY KEY AUTOINCREMENT, goal TEXT NOT NULL,
file TEXT NOT NULL, outcome TEXT, provider TEXT);
CREATE TABLE steps (id INTEGER PRIMARY KEY AUTOINCREMENT, run_id INTEGER NOT NULL,
step INTEGER NOT NULL, decision TEXT NOT NULL, result TEXT NOT NULL,
prompt TEXT NOT NULL DEFAULT '', tool_call TEXT NOT NULL DEFAULT '',
tokens INTEGER NOT NULL DEFAULT 0);
INSERT INTO runs (goal, file) VALUES ('old goal', 'old.txt');",
)
.unwrap();
}
let store = Store::open(&path).unwrap();
assert_eq!(store.last_step(1).unwrap(), 0);
store
.record_event(1, &PolicyEvent::refusal(1, "read", ".env"))
.unwrap();
assert_eq!(store.events(1).unwrap().len(), 1);
}
#[test]
fn a_pending_approval_survives_the_store_being_reopened() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("runs.db");
let request_id = {
let store = Store::open(&path).unwrap();
let run = store.start_run("goal", "root").unwrap();
store
.put_pending(run, 3, "write", "src/a.rs", Some("fn a() {}"))
.unwrap()
};
let store = Store::open(&path).unwrap();
let p = store.pending(request_id).unwrap().expect("still pending");
assert_eq!(p.step, 3);
assert_eq!(p.act, "write");
assert_eq!(p.target, "src/a.rs");
assert_eq!(p.content.as_deref(), Some("fn a() {}"));
assert_eq!(p.resolved, None);
store.resolve_pending(request_id, "approve").unwrap();
let p = store.pending(request_id).unwrap().unwrap();
assert_eq!(p.resolved.as_deref(), Some("approve"));
}
#[test]
fn the_tree_is_reconstructable_from_a_reopened_store() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("runs.db");
let (root, c1, c2, gc) = {
let store = Store::open(&path).unwrap();
let root = store.start_run("root goal", "ws").unwrap();
let c1 = store.start_child_run("child 1", "ws", root, 1).unwrap();
let c2 = store.start_child_run("child 2", "ws", root, 1).unwrap();
let gc = store.start_child_run("grandchild", "ws", c1, 2).unwrap();
store
.record_agent_event(&AgentEvent::spawn(root, 1, c1, "child 1"))
.unwrap();
store
.record_agent_event(&AgentEvent::spawn(root, 1, c2, "child 2"))
.unwrap();
store
.record_agent_event(&AgentEvent::spawn(c1, 1, gc, "grandchild"))
.unwrap();
store
.record_agent_event(&AgentEvent::spawn_refused(root, 2, "agents"))
.unwrap();
store
.record_agent_event(&AgentEvent::budget_draw(c1, 1, 30, 70))
.unwrap();
(root, c1, c2, gc)
};
let store = Store::open(&path).unwrap();
assert_eq!(store.children(root).unwrap(), vec![c1, c2]);
assert_eq!(store.children(c1).unwrap(), vec![gc]);
assert_eq!(store.parent(gc).unwrap(), Some(c1));
assert_eq!(store.parent(root).unwrap(), None);
assert_eq!(store.depth(gc).unwrap(), 2);
let root_events = store.agent_events(root).unwrap();
assert_eq!(root_events.iter().filter(|e| e.kind == "spawn").count(), 2);
assert_eq!(
root_events.iter().filter(|e| e.kind == "spawn_refused").count(),
1
);
let draws = store.agent_events(c1).unwrap();
let draw = draws.iter().find(|e| e.kind == "budget_draw").unwrap();
assert_eq!(draw.tokens, Some(30));
assert_eq!(draw.remaining, Some(70));
}
#[test]
fn a_pre_0_5_database_migrates_and_keeps_its_rows() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("runs.db");
{
let conn = rusqlite::Connection::open(&path).unwrap();
conn.execute_batch(
"CREATE TABLE runs (id INTEGER PRIMARY KEY AUTOINCREMENT, goal TEXT NOT NULL,
file TEXT NOT NULL, outcome TEXT, provider TEXT);
CREATE TABLE steps (id INTEGER PRIMARY KEY AUTOINCREMENT, run_id INTEGER NOT NULL,
step INTEGER NOT NULL, decision TEXT NOT NULL, result TEXT NOT NULL,
prompt TEXT NOT NULL DEFAULT '', tool_call TEXT NOT NULL DEFAULT '',
tokens INTEGER NOT NULL DEFAULT 0);
INSERT INTO runs (goal, file) VALUES ('old', 'old.txt');",
)
.unwrap();
}
let store = Store::open(&path).unwrap();
assert_eq!(store.parent(1).unwrap(), None);
assert_eq!(store.depth(1).unwrap(), 0);
let child = store.start_child_run("c", "ws", 1, 1).unwrap();
assert_eq!(store.children(1).unwrap(), vec![child]);
}
#[test]
fn full_trace_persists_and_reads_back() {
let store = Store::memory().unwrap();
let run = store.start_run("goal", "out.txt").unwrap();
store
.record(
run,
&StepRecord::new(1, "wrote file", "content v1")
.with_trace("the prompt", r#"{"content":"content v1"}"#, 128),
)
.unwrap();
store
.record(run, &StepRecord::new(2, "verified", "ok"))
.unwrap();
store.finish_run(run, "success").unwrap();
let steps = store.steps(run).unwrap();
assert_eq!(steps.len(), 2);
assert_eq!(steps[0].decision, "wrote file");
assert_eq!(steps[0].prompt, "the prompt");
assert_eq!(steps[0].tokens, 128);
assert_eq!(steps[1].result, "ok");
assert_eq!(store.last_step(run).unwrap(), 2);
}
#[test]
fn migrates_a_0_1_0_steps_table_in_place() {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE runs (id INTEGER PRIMARY KEY AUTOINCREMENT, goal TEXT NOT NULL, file TEXT NOT NULL, outcome TEXT);
CREATE TABLE steps (id INTEGER PRIMARY KEY AUTOINCREMENT, run_id INTEGER NOT NULL, step INTEGER NOT NULL, decision TEXT NOT NULL, result TEXT NOT NULL);
INSERT INTO runs (goal, file) VALUES ('g', 'f');
INSERT INTO steps (run_id, step, decision, result) VALUES (1, 1, 'wrote file', 'old');",
)
.unwrap();
let store = Store::from_conn(conn).unwrap();
let steps = store.steps(1).unwrap();
assert_eq!(steps.len(), 1);
assert_eq!(steps[0].result, "old");
assert_eq!(steps[0].prompt, "");
assert_eq!(steps[0].tokens, 0);
}
#[test]
fn provider_is_recorded_and_read_back() {
let store = Store::memory().unwrap();
let run = store.start_run("g", "f").unwrap();
assert_eq!(store.provider(run).unwrap(), None);
store.set_provider(run, "anthropic").unwrap();
assert_eq!(store.provider(run).unwrap().as_deref(), Some("anthropic"));
}
#[test]
fn migrates_a_pre_0_3_runs_table_adding_provider() {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE runs (id INTEGER PRIMARY KEY AUTOINCREMENT, goal TEXT NOT NULL, file TEXT NOT NULL, outcome TEXT);
CREATE TABLE steps (id INTEGER PRIMARY KEY AUTOINCREMENT, run_id INTEGER NOT NULL, step INTEGER NOT NULL, decision TEXT NOT NULL, result TEXT NOT NULL);
INSERT INTO runs (goal, file) VALUES ('g', 'f');",
)
.unwrap();
let store = Store::from_conn(conn).unwrap();
assert_eq!(store.provider(1).unwrap(), None);
store.set_provider(1, "openai").unwrap();
assert_eq!(store.provider(1).unwrap().as_deref(), Some("openai"));
}
}