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_rt
264 .start_empty(
265 run_id.clone(),
266 loop_id,
267 scope,
268 loop_sub,
269 LoopLimits::default(),
270 )
271 .expect("start loop");
272
273 let sink = Arc::new(SyncMemorySink::new());
274 let console_task = if params.render_console {
275 let renderer = Arc::new(ConsoleRenderer::new(
276 ConsoleRendererConfig::default(),
277 sink.clone() as Arc<dyn ConsoleSink>,
278 ));
279 Some(tokio::spawn(async move {
280 let mut sub = console_sub;
281 while let Some(msg) = sub.recv().await {
282 if let Ok(delivered) = msg {
283 renderer.render(&delivered.event);
284 if matches!(delivered.event, InterpreterOutputEvent::Ended(_)) {
285 break;
286 }
287 }
288 }
289 }))
290 } else {
291 drop(console_sub);
292 None
293 };
294
295 let tap_events = Arc::new(Mutex::new(Vec::new()));
296 let tap_events2 = Arc::clone(&tap_events);
297 let tap_task = tokio::spawn(async move {
298 let mut sub = tap_sub;
299 let mut out = Vec::new();
300 while let Some(msg) = sub.recv().await {
301 if let Ok(delivered) = msg {
302 let done = matches!(delivered.event, InterpreterOutputEvent::Ended(_));
303 out.push(delivered.event);
304 if done {
305 break;
306 }
307 }
308 }
309 *tap_events2.lock().await = out;
310 });
311
312 let loop_collect = tokio::spawn(collect_loop_output(loop_handle));
313
314 let pump = {
315 let events = Arc::clone(&interp.events);
316 tokio::spawn(async move {
317 pump_interpreter_to_distributor(events, dist).await;
318 })
319 };
320
321 for chunk in chunks {
322 interp.input.push_bytes(chunk.clone()).await.expect("push");
323 }
324 interp.input.finish_clean().await.expect("finish");
325
326 let _ = pump.await;
327 let (loop_events, loop_end) = loop_collect.await.expect("loop join");
328 let _ = tap_task.await;
329 if let Some(t) = console_task {
330 let _ = t.await;
331 }
332
333 let interpreter_events = tap_events.lock().await.clone();
334 let tools_unavailable = loop_events
335 .iter()
336 .filter(|e| {
337 matches!(
338 e,
339 LoopOutputEvent::OutboundToolResult(r)
340 if r.outcome == OutboundToolOutcome::ToolUnavailable
341 )
342 })
343 .count() as u64;
344
345 let want_html = params.build_html || params.html_dump_path.is_some();
346 let (html_report, html_dump_path) = if want_html {
347 let report = build_html_report(&interpreter_events, ¶ms.html_params);
348 let written = if let Some(ref path) = params.html_dump_path {
349 write_html_report(path, &report).expect("write html dump");
350 Some(path.clone())
351 } else {
352 None
353 };
354 (Some(report), written)
355 } else {
356 (None, None)
357 };
358
359 DriverRunReport {
360 run_id,
361 interpreter_events,
362 loop_events,
363 loop_end,
364 console_text: sink.join(),
365 tools_unavailable,
366 raw_dump,
367 html_report,
368 html_dump_path,
369 }
370}
371
372async fn collect_loop_output(handle: LoopHandle) -> (Vec<LoopOutputEvent>, LoopEnd) {
373 let mut out = Vec::new();
374 {
375 let mut rx = handle.output.lock().await;
376 while let Some(ev) = rx.recv().await {
377 let done = matches!(ev, LoopOutputEvent::LoopEnded(_));
378 out.push(ev);
379 if done {
380 break;
381 }
382 }
383 }
384 let from_stream = out.iter().rev().find_map(|e| match e {
385 LoopOutputEvent::LoopEnded(le) => Some(le.clone()),
386 _ => None,
387 });
388 let loop_end = match from_stream {
389 Some(e) => e,
390 None => handle.completion.wait().await,
391 };
392 (out, loop_end)
393}