1use crate::console::{ConsoleRenderer, ConsoleRendererConfig, ConsoleSink, SyncMemorySink};
7use crate::distribute::{pump_interpreter_to_distributor, EventDistributor, SubscriberPolicy};
8use crate::html_report::{build_html_report, write_html_report, HtmlReport, HtmlReportParams};
9use monoloop_contracts::{
10 DialectBinding, InterpretationId, InterpretationLimits, InterpreterOutputEvent, LoopEnd,
11 LoopId, LoopLimits, LoopOutputEvent, LoopScope, MonoloopRunId, OutboundToolOutcome,
12};
13use monoloop_interpreter::{
14 ConnectionId, DefaultInterpreterFactory, InterpreterFactory, StartInterpretation,
15};
16use monoloop_loop::{DefaultLoopRuntime, LoopHandle};
17use std::path::PathBuf;
18use std::sync::Arc;
19use tokio::sync::Mutex;
20
21#[derive(Clone, Debug)]
23pub struct PipelineParams {
24 pub render_console: bool,
26 pub dump_raw: bool,
28 pub html_dump_path: Option<PathBuf>,
30 pub html_params: HtmlReportParams,
32 pub build_html: bool,
34}
35
36impl Default for PipelineParams {
37 fn default() -> Self {
38 Self {
39 render_console: true,
40 dump_raw: false,
41 html_dump_path: None,
42 html_params: HtmlReportParams::default(),
43 build_html: false,
44 }
45 }
46}
47
48impl PipelineParams {
49 pub fn console_only() -> Self {
51 Self {
52 render_console: true,
53 dump_raw: false,
54 html_dump_path: None,
55 html_params: HtmlReportParams::default(),
56 build_html: false,
57 }
58 }
59
60 pub fn with_raw_dump() -> Self {
62 Self {
63 render_console: true,
64 dump_raw: true,
65 html_dump_path: None,
66 html_params: HtmlReportParams::default(),
67 build_html: false,
68 }
69 }
70
71 pub fn with_html_dump(path: impl Into<PathBuf>) -> Self {
73 Self {
74 render_console: true,
75 dump_raw: false,
76 html_dump_path: Some(path.into()),
77 html_params: HtmlReportParams::default(),
78 build_html: true,
79 }
80 }
81
82 pub fn with_raw_and_html(path: impl Into<PathBuf>) -> Self {
84 Self {
85 render_console: true,
86 dump_raw: true,
87 html_dump_path: Some(path.into()),
88 html_params: HtmlReportParams::default(),
89 build_html: true,
90 }
91 }
92
93 pub fn quiet() -> Self {
95 Self {
96 render_console: false,
97 dump_raw: false,
98 html_dump_path: None,
99 html_params: HtmlReportParams::default(),
100 build_html: false,
101 }
102 }
103}
104
105#[derive(Clone, Debug)]
107pub struct RawInputFrame {
108 pub index: u64,
110 pub bytes: bytes::Bytes,
112}
113
114#[derive(Clone, Debug, Default)]
116pub struct PipelineRawDump {
117 pub frames: Vec<RawInputFrame>,
119}
120
121impl PipelineRawDump {
122 pub fn format_text(&self) -> String {
124 let mut s = String::new();
125 s.push_str(&format!(
126 "=== PIPELINE RAW DUMP (frames={}) ===\n",
127 self.frames.len()
128 ));
129 for f in &self.frames {
130 s.push_str(&format!(
131 "--- chunk #{} len={} ---\n",
132 f.index,
133 f.bytes.len()
134 ));
135 if let Ok(v) = serde_json::from_slice::<serde_json::Value>(&f.bytes) {
136 if let Ok(pretty) = serde_json::to_string_pretty(&v) {
137 s.push_str(&pretty);
138 s.push('\n');
139 continue;
140 }
141 }
142 s.push_str(&String::from_utf8_lossy(&f.bytes));
144 if !s.ends_with('\n') {
145 s.push('\n');
146 }
147 }
148 s.push_str("=== END PIPELINE RAW DUMP ===\n");
149 s
150 }
151
152 pub fn concat(&self) -> bytes::Bytes {
154 let mut out = Vec::new();
155 for f in &self.frames {
156 out.extend_from_slice(&f.bytes);
157 }
158 bytes::Bytes::from(out)
159 }
160
161 pub fn contains_str(&self, needle: &str) -> bool {
163 self.frames
164 .iter()
165 .any(|f| String::from_utf8_lossy(&f.bytes).contains(needle))
166 }
167}
168
169#[derive(Debug)]
171pub struct DriverRunReport {
172 pub run_id: MonoloopRunId,
174 pub interpreter_events: Vec<InterpreterOutputEvent>,
176 pub loop_events: Vec<LoopOutputEvent>,
178 pub loop_end: LoopEnd,
180 pub console_text: String,
182 pub tools_unavailable: u64,
184 pub raw_dump: Option<PipelineRawDump>,
186 pub html_report: Option<HtmlReport>,
188 pub html_dump_path: Option<PathBuf>,
190}
191
192pub async fn run_bytes_pipeline(
194 dialect: DialectBinding,
195 chunks: &[bytes::Bytes],
196 render_console: bool,
197) -> DriverRunReport {
198 run_bytes_pipeline_with_params(
199 dialect,
200 chunks,
201 PipelineParams {
202 render_console,
203 dump_raw: false,
204 html_dump_path: None,
205 html_params: HtmlReportParams::default(),
206 build_html: false,
207 },
208 )
209 .await
210}
211
212pub async fn run_bytes_pipeline_with_params(
214 dialect: DialectBinding,
215 chunks: &[bytes::Bytes],
216 params: PipelineParams,
217) -> DriverRunReport {
218 let run_id = MonoloopRunId::generate();
219 let interpretation_id = InterpretationId::generate();
220 let connection_id = ConnectionId::new("driver-conn");
221 let loop_id = LoopId::generate();
222
223 let raw_dump = if params.dump_raw {
224 Some(PipelineRawDump {
225 frames: chunks
226 .iter()
227 .enumerate()
228 .map(|(i, b)| RawInputFrame {
229 index: i as u64,
230 bytes: b.clone(),
231 })
232 .collect(),
233 })
234 } else {
235 None
236 };
237
238 let factory = DefaultInterpreterFactory::new();
239 let interp = factory
240 .start(StartInterpretation {
241 interpretation_id: interpretation_id.clone(),
242 connection_id: connection_id.clone(),
243 external_session_id: None,
244 dialect,
245 limits: InterpretationLimits::default(),
246 })
247 .expect("start interpretation");
248
249 let mut dist = EventDistributor::new();
250 let loop_sub = dist.subscribe("loop", SubscriberPolicy::Lossless, 1024);
252 let console_sub = dist.subscribe("console", SubscriberPolicy::BestEffort, 1024);
253 let tap_sub = dist.subscribe("tap", SubscriberPolicy::Lossless, 4096);
254
255 let loop_rt = DefaultLoopRuntime::new();
256 let scope = LoopScope::single(
257 run_id.clone(),
258 loop_id.clone(),
259 interpretation_id,
260 connection_id,
261 None,
262 );
263 let (loop_handle, loop_fut) = loop_rt
265 .prepare_empty(
266 run_id.clone(),
267 loop_id,
268 scope,
269 loop_sub,
270 LoopLimits::default(),
271 )
272 .expect("prepare loop");
273 tokio::spawn(loop_fut);
274
275 let sink = Arc::new(SyncMemorySink::new());
276 let console_task = if params.render_console {
277 let renderer = Arc::new(ConsoleRenderer::new(
278 ConsoleRendererConfig::default(),
279 sink.clone() as Arc<dyn ConsoleSink>,
280 ));
281 Some(tokio::spawn(async move {
282 let mut sub = console_sub;
283 while let Some(msg) = sub.recv().await {
284 if let Ok(delivered) = msg {
285 renderer.render(&delivered.event);
286 if matches!(delivered.event, InterpreterOutputEvent::Ended(_)) {
287 break;
288 }
289 }
290 }
291 }))
292 } else {
293 drop(console_sub);
294 None
295 };
296
297 let tap_events = Arc::new(Mutex::new(Vec::new()));
298 let tap_events2 = Arc::clone(&tap_events);
299 let tap_task = tokio::spawn(async move {
300 let mut sub = tap_sub;
301 let mut out = Vec::new();
302 while let Some(msg) = sub.recv().await {
303 if let Ok(delivered) = msg {
304 let done = matches!(delivered.event, InterpreterOutputEvent::Ended(_));
305 out.push(delivered.event);
306 if done {
307 break;
308 }
309 }
310 }
311 *tap_events2.lock().await = out;
312 });
313
314 let loop_collect = tokio::spawn(collect_loop_output(loop_handle));
315
316 let pump = {
317 let events = Arc::clone(&interp.events);
318 tokio::spawn(async move {
319 pump_interpreter_to_distributor(events, dist).await;
320 })
321 };
322
323 for chunk in chunks {
324 interp.input.push_bytes(chunk.clone()).await.expect("push");
325 }
326 interp.input.finish_clean().await.expect("finish");
327
328 let _ = pump.await;
329 let (loop_events, loop_end) = loop_collect.await.expect("loop join");
330 let _ = tap_task.await;
331 if let Some(t) = console_task {
332 let _ = t.await;
333 }
334
335 let interpreter_events = tap_events.lock().await.clone();
336 let tools_unavailable = loop_events
337 .iter()
338 .filter(|e| {
339 matches!(
340 e,
341 LoopOutputEvent::OutboundToolResult(r)
342 if r.outcome == OutboundToolOutcome::ToolUnavailable
343 )
344 })
345 .count() as u64;
346
347 let want_html = params.build_html || params.html_dump_path.is_some();
348 let (html_report, html_dump_path) = if want_html {
349 let report = build_html_report(&interpreter_events, ¶ms.html_params);
350 let written = if let Some(ref path) = params.html_dump_path {
351 write_html_report(path, &report).expect("write html dump");
352 Some(path.clone())
353 } else {
354 None
355 };
356 (Some(report), written)
357 } else {
358 (None, None)
359 };
360
361 DriverRunReport {
362 run_id,
363 interpreter_events,
364 loop_events,
365 loop_end,
366 console_text: sink.join(),
367 tools_unavailable,
368 raw_dump,
369 html_report,
370 html_dump_path,
371 }
372}
373
374async fn collect_loop_output(handle: LoopHandle) -> (Vec<LoopOutputEvent>, LoopEnd) {
375 let mut out = Vec::new();
376 {
377 let mut rx = handle.take_output().await;
378 while let Some(ev) = rx.recv().await {
379 let done = matches!(ev, LoopOutputEvent::LoopEnded(_));
380 out.push(ev);
381 if done {
382 break;
383 }
384 }
385 }
386 let from_stream = out.iter().rev().find_map(|e| match e {
387 LoopOutputEvent::LoopEnded(le) => Some(le.clone()),
388 _ => None,
389 });
390 let loop_end = match from_stream {
391 Some(e) => e,
392 None => handle.completion.wait().await,
393 };
394 (out, loop_end)
395}