1pub mod agent_sdk;
8pub mod backend;
9pub mod backfill;
10pub mod http;
11pub mod llm_backend;
12pub mod prompt;
13pub mod scope;
14pub mod state;
15
16use crate::dream::backend::{BackfillInput, DreamBackend};
17
18pub struct DreamOptions {
19 pub project_hash: String,
20 pub dry_run: bool,
22}
23
24#[derive(Debug, Default, PartialEq)]
25pub struct DreamReport {
26 pub sessions_processed: usize,
27 pub events_backfilled: usize,
28}
29
30pub fn run_dream(
34 conn: &rusqlite::Connection,
35 events_path: &std::path::Path,
36 opts: &DreamOptions,
37 backend: &dyn DreamBackend,
38 sessions: Vec<(String, BackfillInput)>,
39 run_id: &str,
40) -> anyhow::Result<DreamReport> {
41 let mut report = DreamReport::default();
42 for (session_id, input) in sessions {
43 report.sessions_processed += 1;
44 if opts.dry_run {
45 continue;
46 }
47 let proposed = backend.backfill(&input)?;
48 let existing: Vec<String> = input
50 .tasks
51 .iter()
52 .flat_map(|t| t.existing_events.clone())
53 .collect();
54 let kept = crate::dream::backfill::dedup_guard(proposed, &existing);
55 let mut writer = crate::storage::JsonlWriter::open(events_path)?;
56 for b in &kept {
57 let e = crate::dream::backfill::to_event(b, run_id, &session_id);
58 writer.append(&e)?;
59 crate::db::upsert_task_from_event(conn, &e, &opts.project_hash)?;
60 crate::db::index_event(conn, &e)?;
61 report.events_backfilled += 1;
62 }
63 writer.flush_durable()?;
64 }
65 Ok(report)
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use crate::dream::backend::{BackfillEvent, BackfillTaskContext, MockDreamBackend};
72 use crate::event::{Author, Event, EventType, Source};
73 use tempfile::TempDir;
74
75 fn task_input() -> (String, BackfillInput) {
76 (
77 "sess-1".to_string(),
78 BackfillInput {
79 tasks: vec![BackfillTaskContext {
80 task_id: "tj-1".into(),
81 title: "Demo".into(),
82 existing_events: vec!["Already known fact.".into()],
83 }],
84 transcript: "user: ...\nassistant: ...".into(),
85 },
86 )
87 }
88
89 #[test]
90 fn run_dream_appends_novel_events_and_indexes() {
91 let d = TempDir::new().unwrap();
92 let conn = crate::db::open(d.path().join("s.sqlite")).unwrap();
93 let events_path = d.path().join("events.jsonl");
94
95 let open = Event::new(
97 "tj-1",
98 EventType::Open,
99 Author::User,
100 Source::Cli,
101 "Demo".into(),
102 );
103 crate::db::upsert_task_from_event(&conn, &open, "ph").unwrap();
104
105 let backend = MockDreamBackend {
106 events: vec![
107 BackfillEvent {
108 event_type: EventType::Finding,
109 task_id: "tj-1".into(),
110 text: "A brand new finding.".into(),
111 timestamp: "2026-06-08T10:00:00Z".into(),
112 },
113 BackfillEvent {
114 event_type: EventType::Finding,
115 task_id: "tj-1".into(),
116 text: "Already known fact.".into(), timestamp: "2026-06-08T10:01:00Z".into(),
118 },
119 ],
120 };
121 let opts = DreamOptions {
122 project_hash: "ph".into(),
123 dry_run: false,
124 };
125 let report = run_dream(
126 &conn,
127 &events_path,
128 &opts,
129 &backend,
130 vec![task_input()],
131 "run-1",
132 )
133 .unwrap();
134
135 assert_eq!(report.sessions_processed, 1);
136 assert_eq!(report.events_backfilled, 1); let body = std::fs::read_to_string(&events_path).unwrap();
138 assert!(body.contains("A brand new finding."));
139 assert!(body.contains("\"source\":\"dream\""));
140 assert!(!body.contains("\"text\":\"Already known fact.\",\"refs\""));
141 }
142
143 #[test]
144 fn dry_run_writes_nothing_and_skips_backend() {
145 let d = TempDir::new().unwrap();
146 let conn = crate::db::open(d.path().join("s.sqlite")).unwrap();
147 let events_path = d.path().join("events.jsonl");
148 let backend = MockDreamBackend { events: vec![] };
149 let opts = DreamOptions {
150 project_hash: "ph".into(),
151 dry_run: true,
152 };
153 let report = run_dream(
154 &conn,
155 &events_path,
156 &opts,
157 &backend,
158 vec![task_input()],
159 "run-1",
160 )
161 .unwrap();
162 assert_eq!(report.sessions_processed, 1);
163 assert_eq!(report.events_backfilled, 0);
164 assert!(!events_path.exists());
165 }
166}