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