1use crate::console::{ConsoleRenderer, ConsoleRendererConfig, ConsoleSink, SyncMemorySink};
8use crate::grok_serve::{GrokServeOptions, ManagedGrokServe};
9use crate::html_report::{build_html_report, write_html_report, HtmlReport, HtmlReportParams};
10use monoloop_connector_grok::{
11 EncodedAcpSessionMessage, GrokConnector, GrokConnectorLimits, GrokServerConfig,
12 GrokSessionConfig, InMemorySecretResolver, RawDumpCollector, RawDumpSnapshot, SecretRef,
13};
14use monoloop_contracts::{
15 CanonicalUnit, DialectBinding, DialectDescriptor, InterpretationId, InterpretationLimits,
16 InterpreterOutputEvent,
17};
18use monoloop_interpreter::{DefaultInterpreterFactory, InterpreterFactory, StartInterpretation};
19use std::path::PathBuf;
20use std::sync::Arc;
21use std::time::Duration;
22
23#[derive(Clone, Debug)]
25pub struct LiveGrokRunOptions {
26 pub prompt: String,
28 pub cwd: PathBuf,
30 pub serve: GrokServeOptions,
32 pub title: String,
34 pub artifact_stem: PathBuf,
37 pub request_deadline: Duration,
40 pub prompt_wait_ceiling: Option<Duration>,
43 pub connect_timeout: Duration,
45 pub render_console: bool,
47}
48
49impl LiveGrokRunOptions {
50 pub fn for_project(project: impl Into<PathBuf>, prompt: impl Into<String>) -> Self {
52 let project = project.into();
53 let log = project.join("target/grok-serve.managed.log");
54 Self {
55 prompt: prompt.into(),
56 cwd: project.clone(),
57 serve: GrokServeOptions {
58 port: None, secret: std::env::var("GROK_AGENT_SECRET")
60 .unwrap_or_else(|_| "monoloop-live-test".into()),
61 grok_bin: PathBuf::from(
62 std::env::var("GROK_BIN").unwrap_or_else(|_| "grok".into()),
63 ),
64 ready_timeout: Duration::from_secs(15),
65 log_path: Some(log),
66 },
67 title: "Live Grok Build — interpretation review".into(),
68 artifact_stem: project.join("target/live_grok_run"),
69 request_deadline: Duration::from_secs(2 * 60 * 60),
70 prompt_wait_ceiling: None,
71 connect_timeout: Duration::from_secs(30),
72 render_console: true,
73 }
74 }
75}
76
77#[derive(Clone, Debug)]
79pub struct LiveGrokRunReport {
80 pub session_id: String,
82 pub prompt_result: String,
84 pub timed_out: bool,
86 pub events: Vec<InterpreterOutputEvent>,
88 pub html: HtmlReport,
90 pub raw: RawDumpSnapshot,
92 pub console_text: String,
94 pub sequence_text: String,
96 pub paths: LiveGrokArtifactPaths,
98 pub port: u16,
100}
101
102#[derive(Clone, Debug)]
104pub struct LiveGrokArtifactPaths {
105 pub html: PathBuf,
107 pub raw: PathBuf,
109 pub sequence: PathBuf,
111 pub chat: PathBuf,
113}
114
115pub async fn run_live_grok_prompt(opts: LiveGrokRunOptions) -> Result<LiveGrokRunReport, String> {
119 if let Some(parent) = opts.artifact_stem.parent() {
120 if !parent.as_os_str().is_empty() {
121 std::fs::create_dir_all(parent)
122 .map_err(|e| format!("create artifact dir {}: {e}", parent.display()))?;
123 }
124 }
125
126 println!(
127 "live-grok: starting managed serve (ready ≤ {:?})…",
128 opts.serve.ready_timeout
129 );
130 let serve = ManagedGrokServe::start(opts.serve.clone()).await?;
131 let port = serve.port();
132 let secret = serve.secret().to_string();
133 println!("live-grok: serve up pid={:?} port={port}", serve.pid());
134
135 let result = run_session_with_serve(&opts, &serve, &secret, port).await;
136
137 println!("live-grok: stopping serve…");
138 if let Err(e) = serve.stop().await {
139 eprintln!("live-grok: serve stop warning: {e}");
140 } else {
141 println!("live-grok: serve stopped");
142 }
143
144 result
145}
146
147async fn run_session_with_serve(
148 opts: &LiveGrokRunOptions,
149 _serve: &ManagedGrokServe,
150 secret: &str,
151 port: u16,
152) -> Result<LiveGrokRunReport, String> {
153 let secrets = Arc::new(InMemorySecretResolver::new());
154 secrets.insert("GROK_WS_SECRET", secret);
155 let dump = Arc::new(RawDumpCollector::enabled());
156
157 let mut limits = GrokConnectorLimits::default();
158 limits.request_deadline = opts.request_deadline;
159 limits.connect_deadline = opts.connect_timeout;
160
161 let mut config = GrokServerConfig::loopback(port, SecretRef::new("GROK_WS_SECRET"))
162 .map_err(|e| format!("server config: {e}"))?;
163 config.limits = limits;
164 let config = config.with_raw_dump(Arc::clone(&dump));
165
166 let connector = GrokConnector::new(secrets);
167 println!("live-grok: connecting ws://127.0.0.1:{port}/ws …");
168 let pending = connector
169 .connect(config)
170 .map_err(|e| format!("connect begin: {e}"))?;
171 let server = tokio::time::timeout(opts.connect_timeout, pending.wait())
172 .await
173 .map_err(|_| "connect timed out".to_string())?
174 .map_err(|e| format!("connect failed: {e}"))?;
175 println!("live-grok: connected + initialized");
176
177 let pending_sess = server
178 .sessions
179 .begin_new(GrokSessionConfig {
180 cwd: Some(opts.cwd.display().to_string()),
181 mcp_servers: vec![],
182 permission_mode: Some("always-approve".into()),
183 agent_profile: None,
184 extension_metadata: Some(serde_json::json!({ "yoloMode": true })),
185 })
186 .map_err(|e| format!("session/new begin: {e}"))?;
187 let session = tokio::time::timeout(opts.connect_timeout, pending_sess.wait())
188 .await
189 .map_err(|_| "session/new timed out".to_string())?
190 .map_err(|e| format!("session/new failed: {e}"))?;
191 let session_id = session.session_id.as_str().to_string();
192 println!("live-grok: sessionId={session_id}");
193
194 let factory = DefaultInterpreterFactory::new();
195 let interp = factory
196 .start(StartInterpretation {
197 interpretation_id: InterpretationId::generate(),
198 connection_id: session.connection_id.clone(),
199 external_session_id: Some(session.session_id.clone().into()),
200 dialect: DialectBinding::negotiated(DialectDescriptor::acp_json_rpc("1")),
201 limits: InterpretationLimits::default(),
202 })
203 .map_err(|e| format!("start interpretation: {e}"))?;
204
205 let sink = Arc::new(SyncMemorySink::new());
206 let renderer = ConsoleRenderer::new(
207 ConsoleRendererConfig {
208 show_tool_payloads: true,
209 max_content_chars: 2000,
210 ..Default::default()
211 },
212 sink.clone() as Arc<dyn ConsoleSink>,
213 );
214
215 let output = Arc::clone(&session.output);
216 let input = interp.input.clone();
217 let drain = tokio::spawn(async move {
218 loop {
219 match output.receive().await {
220 Ok(Some(bytes)) => {
221 if input.push_bytes(bytes).await.is_err() {
222 break;
223 }
224 }
225 Ok(None) => break,
226 Err(_) => break,
227 }
228 }
229 });
230
231 println!(
232 "live-grok: session/prompt (request_deadline={:?}, outer_ceiling={:?})…",
233 opts.request_deadline, opts.prompt_wait_ceiling
234 );
235 let exchange = session
236 .input
237 .begin_send(EncodedAcpSessionMessage {
238 method: "session/prompt".into(),
239 params: serde_json::json!({
240 "prompt": [
241 { "type": "text", "text": opts.prompt }
242 ]
243 }),
244 })
245 .map_err(|e| format!("begin_send: {e}"))?;
246
247 let (prompt_result, timed_out) = match opts.prompt_wait_ceiling {
248 Some(ceiling) => match tokio::time::timeout(ceiling, exchange.wait()).await {
249 Ok(Ok(v)) => {
250 println!("live-grok: prompt complete");
251 (format!("{v}"), false)
252 }
253 Ok(Err(e)) => (format!("error:{e}"), false),
254 Err(_) => {
255 eprintln!(
256 "live-grok: outer ceiling {:?} hit — salvaging streamed events",
257 ceiling
258 );
259 ("timeout".into(), true)
260 }
261 },
262 None => match exchange.wait().await {
263 Ok(v) => {
264 println!("live-grok: prompt complete");
265 (format!("{v}"), false)
266 }
267 Err(e) => (format!("error:{e}"), false),
268 },
269 };
270
271 tokio::time::sleep(Duration::from_millis(400)).await;
273 session
274 .control
275 .cancel(monoloop_connector_grok::CancellationReason::CallerRequested);
276 let _ = tokio::time::timeout(Duration::from_secs(3), drain).await;
277 let _ = interp.input.finish_clean().await;
278
279 let mut events = Vec::new();
280 loop {
281 match tokio::time::timeout(Duration::from_secs(2), interp.events.recv()).await {
282 Ok(Some(ev)) => {
283 if opts.render_console {
284 renderer.render(&ev);
285 }
286 let done = matches!(ev, InterpreterOutputEvent::Ended(_));
287 events.push(ev);
288 if done {
289 break;
290 }
291 }
292 Ok(None) => break,
293 Err(_) => break,
294 }
295 }
296
297 let raw = dump.snapshot();
298 let html = build_html_report(
299 &events,
300 &HtmlReportParams {
301 title: opts.title.clone(),
302 show_tool_payloads: true,
303 ..Default::default()
304 },
305 );
306
307 let sequence_text = format_sequence(&session_id, &prompt_result, &events);
308 let paths = LiveGrokArtifactPaths {
309 html: PathBuf::from(format!("{}.html", opts.artifact_stem.display())),
310 raw: PathBuf::from(format!("{}.raw.txt", opts.artifact_stem.display())),
311 sequence: PathBuf::from(format!("{}.sequence.txt", opts.artifact_stem.display())),
312 chat: PathBuf::from(format!("{}.chat.txt", opts.artifact_stem.display())),
313 };
314
315 std::fs::write(&paths.raw, raw.format_text()).map_err(|e| format!("write raw: {e}"))?;
316 std::fs::write(&paths.sequence, &sequence_text).map_err(|e| format!("write sequence: {e}"))?;
317 write_html_report(&paths.html, &html).map_err(|e| format!("write html: {e}"))?;
318 std::fs::write(&paths.chat, &html.chat_projection.plain_text)
319 .map_err(|e| format!("write chat: {e}"))?;
320
321 Ok(LiveGrokRunReport {
322 session_id,
323 prompt_result,
324 timed_out,
325 events,
326 html,
327 raw,
328 console_text: sink.join(),
329 sequence_text,
330 paths,
331 port,
332 })
333}
334
335fn format_sequence(
336 session_id: &str,
337 prompt_result: &str,
338 events: &[InterpreterOutputEvent],
339) -> String {
340 let mut sequence = String::new();
341 sequence.push_str("=== LIVE GROK MANAGED RUN — CANONICAL EVENT SEQUENCE ===\n");
342 sequence.push_str(&format!("sessionId={session_id}\n"));
343 sequence.push_str(&format!("prompt_result={prompt_result}\n\n"));
344 for (i, ev) in events.iter().enumerate() {
345 sequence.push_str(&format!("{:04} {}\n", i, describe_event(ev)));
346 }
347 sequence.push_str(&format!("\n=== total events: {} ===\n", events.len()));
348 sequence.push_str("\n=== TOOL ACTIONS (compressed) ===\n");
349 for line in tool_summary(events) {
350 sequence.push_str(&line);
351 sequence.push('\n');
352 }
353 sequence
354}
355
356fn describe_event(ev: &InterpreterOutputEvent) -> String {
357 match ev {
358 InterpreterOutputEvent::Unit(u) => {
359 let s = u.snapshot();
360 match &s.unit {
361 CanonicalUnit::Text(t) => format!(
362 "TEXT ch={:?} g={} | {}",
363 t.channel,
364 s.unit_generation,
365 truncate(&t.content, 120)
366 ),
367 CanonicalUnit::Tool(t) => format!(
368 "TOOL action={} name={:?} req={:?} exec={:?} g={} wait={:?} args={}",
369 t.tool_action_id.as_str(),
370 t.tool_name,
371 t.request_state,
372 t.execution_state,
373 s.unit_generation,
374 t.waiting_for,
375 t.request_payload
376 .as_deref()
377 .map(|p| truncate(p, 80))
378 .unwrap_or_else(|| "-".into())
379 ),
380 CanonicalUnit::Boundary(b) => format!("BOUNDARY {:?}", b.kind),
381 CanonicalUnit::Structure(st) => {
382 format!("STRUCTURE {:?} | {}", st.kind, truncate(&st.content, 80))
383 }
384 CanonicalUnit::Diagnostic(d) => {
385 format!("DIAG {:?} | {}", d.kind, truncate(&d.message, 100))
386 }
387 CanonicalUnit::Paragraph(p) => format!("PARAGRAPH {:?}", p.kind),
388 CanonicalUnit::Usage(u) => format!("USAGE {u:?}"),
389 }
390 }
391 InterpreterOutputEvent::Ended(e) => format!(
392 "END {:?} events={} sentences={} unresolved={}",
393 e.kind, e.canonical_event_count, e.completed_sentence_count, e.unresolved_text_bytes
394 ),
395 }
396}
397
398fn tool_summary(events: &[InterpreterOutputEvent]) -> Vec<String> {
399 use std::collections::BTreeMap;
400 let mut map: BTreeMap<String, Vec<String>> = BTreeMap::new();
401 for ev in events {
402 if let InterpreterOutputEvent::Unit(u) = ev {
403 if let CanonicalUnit::Tool(t) = &u.snapshot().unit {
404 let id = t.tool_action_id.as_str().to_string();
405 map.entry(id).or_default().push(format!(
406 "g{} {:?} name={:?} terminal={:?}",
407 u.snapshot().unit_generation,
408 t.request_state,
409 t.tool_name,
410 t.terminal_outcome
411 ));
412 }
413 }
414 }
415 map.into_iter()
416 .map(|(id, gens)| format!("{id}: {}", gens.join(" → ")))
417 .collect()
418}
419
420fn truncate(s: &str, max: usize) -> String {
421 let t: String = s.chars().take(max).collect();
422 if s.chars().count() > max {
423 format!("{t}…")
424 } else {
425 t
426 }
427}
428
429#[derive(Clone, Debug)]
434pub struct LiveGrokMultiSessionOptions {
435 pub project: PathBuf,
437 pub prompt_a: String,
439 pub prompt_b: String,
441 pub marker_a: String,
443 pub marker_b: String,
445 pub base: LiveGrokRunOptions,
447 pub verify_explicit_load: bool,
450}
451
452impl LiveGrokMultiSessionOptions {
453 pub fn for_project(project: impl Into<PathBuf>) -> Self {
455 let project = project.into();
456 let mut base = LiveGrokRunOptions::for_project(&project, "unused");
457 base.artifact_stem = project.join("target/live_grok_multi_session");
458 base.title = "Live Grok multi-session qualification".into();
459 base.prompt_wait_ceiling = Some(Duration::from_secs(180));
461 base.request_deadline = Duration::from_secs(180);
462 Self {
463 project,
464 prompt_a: "Reply with exactly this token and nothing else: MONOLOOP_MS_A".into(),
465 prompt_b: "Reply with exactly this token and nothing else: MONOLOOP_MS_B".into(),
466 marker_a: "MONOLOOP_MS_A".into(),
467 marker_b: "MONOLOOP_MS_B".into(),
468 base,
469 verify_explicit_load: true,
470 }
471 }
472}
473
474#[derive(Clone, Debug)]
476pub struct LiveGrokSessionOutcome {
477 pub session_id: String,
479 pub prompt_result: String,
481 pub timed_out: bool,
483 pub events: Vec<InterpreterOutputEvent>,
485 pub chat_text: String,
487}
488
489#[derive(Clone, Debug)]
491pub struct LiveGrokMultiSessionReport {
492 pub port: u16,
494 pub session_a: LiveGrokSessionOutcome,
496 pub session_b: LiveGrokSessionOutcome,
498 pub load_a_ok: Option<bool>,
500 pub summary_path: PathBuf,
502}
503
504pub async fn run_live_grok_multi_session(
509 opts: LiveGrokMultiSessionOptions,
510) -> Result<LiveGrokMultiSessionReport, String> {
511 if let Some(parent) = opts.base.artifact_stem.parent() {
512 if !parent.as_os_str().is_empty() {
513 std::fs::create_dir_all(parent)
514 .map_err(|e| format!("create artifact dir {}: {e}", parent.display()))?;
515 }
516 }
517
518 println!(
519 "live-grok-multi: starting managed serve (ready ≤ {:?})…",
520 opts.base.serve.ready_timeout
521 );
522 let serve = ManagedGrokServe::start(opts.base.serve.clone()).await?;
523 let port = serve.port();
524 let secret = serve.secret().to_string();
525 println!(
526 "live-grok-multi: serve up pid={:?} port={port}",
527 serve.pid()
528 );
529
530 let result = run_multi_with_serve(&opts, &secret, port).await;
531
532 println!("live-grok-multi: stopping serve…");
533 if let Err(e) = serve.stop().await {
534 eprintln!("live-grok-multi: serve stop warning: {e}");
535 } else {
536 println!("live-grok-multi: serve stopped");
537 }
538
539 result
540}
541
542async fn run_multi_with_serve(
543 opts: &LiveGrokMultiSessionOptions,
544 secret: &str,
545 port: u16,
546) -> Result<LiveGrokMultiSessionReport, String> {
547 use monoloop_connector_grok::{GrokSessionId, GrokSessionLoadConfig};
548
549 let secrets = Arc::new(InMemorySecretResolver::new());
550 secrets.insert("GROK_WS_SECRET", secret);
551 let dump = Arc::new(RawDumpCollector::enabled());
552
553 let mut limits = GrokConnectorLimits::default();
554 limits.request_deadline = opts.base.request_deadline;
555 limits.connect_deadline = opts.base.connect_timeout;
556
557 let mut config = GrokServerConfig::loopback(port, SecretRef::new("GROK_WS_SECRET"))
558 .map_err(|e| format!("server config: {e}"))?;
559 config.limits = limits;
560 let config = config.with_raw_dump(Arc::clone(&dump));
561
562 let connector = GrokConnector::new(secrets);
563 println!("live-grok-multi: connecting ws://127.0.0.1:{port}/ws …");
564 let pending = connector
565 .connect(config)
566 .map_err(|e| format!("connect begin: {e}"))?;
567 let server = Arc::new(
568 tokio::time::timeout(opts.base.connect_timeout, pending.wait())
569 .await
570 .map_err(|_| "connect timed out".to_string())?
571 .map_err(|e| format!("connect failed: {e}"))?,
572 );
573 println!("live-grok-multi: connected + initialized");
574
575 let session_cfg = GrokSessionConfig {
576 cwd: Some(opts.project.display().to_string()),
577 mcp_servers: vec![],
578 permission_mode: Some("always-approve".into()),
579 agent_profile: None,
580 extension_metadata: Some(serde_json::json!({ "yoloMode": true })),
581 };
582
583 let cfg_a = session_cfg.clone();
585 let cfg_b = session_cfg.clone();
586 let server_a = Arc::clone(&server);
587 let server_b = Arc::clone(&server);
588 let connect_timeout = opts.base.connect_timeout;
589 let (sess_a, sess_b) = tokio::try_join!(
590 async move {
591 let pending = server_a
592 .sessions
593 .begin_new(cfg_a)
594 .map_err(|e| format!("session A begin_new: {e}"))?;
595 tokio::time::timeout(connect_timeout, pending.wait())
596 .await
597 .map_err(|_| "session A new timed out".to_string())?
598 .map_err(|e| format!("session A new failed: {e}"))
599 },
600 async move {
601 let pending = server_b
602 .sessions
603 .begin_new(cfg_b)
604 .map_err(|e| format!("session B begin_new: {e}"))?;
605 tokio::time::timeout(connect_timeout, pending.wait())
606 .await
607 .map_err(|_| "session B new timed out".to_string())?
608 .map_err(|e| format!("session B new failed: {e}"))
609 }
610 )?;
611
612 let id_a = sess_a.session_id.as_str().to_string();
613 let id_b = sess_b.session_id.as_str().to_string();
614 if id_a == id_b {
615 return Err(format!(
616 "concurrent session/new must yield distinct sessionIds; both={id_a}"
617 ));
618 }
619 println!("live-grok-multi: sessionA={id_a}");
620 println!("live-grok-multi: sessionB={id_b}");
621
622 let prompt_a = opts.prompt_a.clone();
623 let prompt_b = opts.prompt_b.clone();
624 let ceiling = opts.base.prompt_wait_ceiling;
625 let render = opts.base.render_console;
626
627 let (out_a, out_b) = tokio::try_join!(
628 prompt_one_session(sess_a, prompt_a, ceiling, render),
629 prompt_one_session(sess_b, prompt_b, ceiling, render),
630 )?;
631
632 if !out_a.timed_out {
634 let has_a = out_a.chat_text.contains(&opts.marker_a)
635 || out_a
636 .events
637 .iter()
638 .any(|e| event_text(e).contains(&opts.marker_a));
639 if !has_a {
640 return Err(format!(
641 "session A missing marker {}; chat={}",
642 opts.marker_a,
643 truncate(&out_a.chat_text, 200)
644 ));
645 }
646 if out_a.chat_text.contains(&opts.marker_b) {
647 return Err(format!(
648 "session A chat must not contain B marker {}; chat={}",
649 opts.marker_b,
650 truncate(&out_a.chat_text, 200)
651 ));
652 }
653 }
654 if !out_b.timed_out {
655 let has_b = out_b.chat_text.contains(&opts.marker_b)
656 || out_b
657 .events
658 .iter()
659 .any(|e| event_text(e).contains(&opts.marker_b));
660 if !has_b {
661 return Err(format!(
662 "session B missing marker {}; chat={}",
663 opts.marker_b,
664 truncate(&out_b.chat_text, 200)
665 ));
666 }
667 if out_b.chat_text.contains(&opts.marker_a) {
668 return Err(format!(
669 "session B chat must not contain A marker {}; chat={}",
670 opts.marker_a,
671 truncate(&out_b.chat_text, 200)
672 ));
673 }
674 }
675
676 let load_a_ok = if opts.verify_explicit_load {
677 tokio::time::sleep(Duration::from_millis(500)).await;
679 let known = GrokSessionId::new(id_a.clone());
680 let load_cfg = GrokSessionLoadConfig {
681 cwd: Some(opts.project.display().to_string()),
682 };
683 match server.sessions.begin_load(known, load_cfg) {
684 Err(e) => {
685 eprintln!("live-grok-multi: session/load begin failed: {e}");
686 Some(false)
687 }
688 Ok(pending) => {
689 match tokio::time::timeout(opts.base.connect_timeout, pending.wait()).await {
690 Err(_) => {
691 eprintln!("live-grok-multi: session/load timed out");
692 Some(false)
693 }
694 Ok(Err(e)) => {
695 eprintln!("live-grok-multi: session/load failed: {e}");
699 Some(false)
700 }
701 Ok(Ok(loaded)) => {
702 let loaded_id = loaded.session_id.as_str().to_string();
703 loaded
704 .control
705 .cancel(monoloop_connector_grok::CancellationReason::CallerRequested);
706 let ok = loaded_id == id_a;
707 println!("live-grok-multi: explicit load of A → {loaded_id} (ok={ok})");
708 Some(ok)
709 }
710 }
711 }
712 }
713 } else {
714 None
715 };
716
717 let summary_path = PathBuf::from(format!("{}.summary.txt", opts.base.artifact_stem.display()));
718 let mut summary = String::new();
719 summary.push_str("=== LIVE GROK MULTI-SESSION QUALIFICATION ===\n");
720 summary.push_str(&format!("port={port}\n"));
721 summary.push_str(&format!(
722 "session_a id={} timed_out={} prompt_result={}\n",
723 out_a.session_id, out_a.timed_out, out_a.prompt_result
724 ));
725 summary.push_str(&format!(
726 "session_b id={} timed_out={} prompt_result={}\n",
727 out_b.session_id, out_b.timed_out, out_b.prompt_result
728 ));
729 summary.push_str(&format!("load_a_ok={load_a_ok:?}\n"));
730 summary.push_str(&format!(
731 "distinct_ids={}\n",
732 out_a.session_id != out_b.session_id
733 ));
734 std::fs::write(&summary_path, &summary).map_err(|e| format!("write summary: {e}"))?;
735 let _ = dump.snapshot(); Ok(LiveGrokMultiSessionReport {
738 port,
739 session_a: out_a,
740 session_b: out_b,
741 load_a_ok,
742 summary_path,
743 })
744}
745
746async fn prompt_one_session(
747 session: monoloop_connector_grok::GrokSessionHandle,
748 prompt: String,
749 ceiling: Option<Duration>,
750 render_console: bool,
751) -> Result<LiveGrokSessionOutcome, String> {
752 let session_id = session.session_id.as_str().to_string();
753 let factory = DefaultInterpreterFactory::new();
754 let interp = factory
755 .start(StartInterpretation {
756 interpretation_id: InterpretationId::generate(),
757 connection_id: session.connection_id.clone(),
758 external_session_id: Some(session.session_id.clone().into()),
759 dialect: DialectBinding::negotiated(DialectDescriptor::acp_json_rpc("1")),
760 limits: InterpretationLimits::default(),
761 })
762 .map_err(|e| format!("start interpretation {session_id}: {e}"))?;
763
764 let sink = Arc::new(SyncMemorySink::new());
765 let renderer = ConsoleRenderer::new(
766 ConsoleRendererConfig {
767 show_tool_payloads: false,
768 max_content_chars: 500,
769 ..Default::default()
770 },
771 sink.clone() as Arc<dyn ConsoleSink>,
772 );
773
774 let output = Arc::clone(&session.output);
775 let input = interp.input.clone();
776 let drain = tokio::spawn(async move {
777 loop {
778 match output.receive().await {
779 Ok(Some(bytes)) => {
780 if input.push_bytes(bytes).await.is_err() {
781 break;
782 }
783 }
784 Ok(None) => break,
785 Err(_) => break,
786 }
787 }
788 });
789
790 println!("live-grok-multi: prompt sessionId={session_id}…");
791 let exchange = session
792 .input
793 .begin_send(EncodedAcpSessionMessage {
794 method: "session/prompt".into(),
795 params: serde_json::json!({
796 "prompt": [ { "type": "text", "text": prompt } ]
797 }),
798 })
799 .map_err(|e| format!("begin_send {session_id}: {e}"))?;
800
801 let (prompt_result, timed_out) = match ceiling {
802 Some(ceiling) => match tokio::time::timeout(ceiling, exchange.wait()).await {
803 Ok(Ok(v)) => (format!("{v}"), false),
804 Ok(Err(e)) => (format!("error:{e}"), false),
805 Err(_) => ("timeout".into(), true),
806 },
807 None => match exchange.wait().await {
808 Ok(v) => (format!("{v}"), false),
809 Err(e) => (format!("error:{e}"), false),
810 },
811 };
812
813 tokio::time::sleep(Duration::from_millis(400)).await;
814 session
815 .control
816 .cancel(monoloop_connector_grok::CancellationReason::CallerRequested);
817 let _ = tokio::time::timeout(Duration::from_secs(3), drain).await;
818 let _ = interp.input.finish_clean().await;
819
820 let mut events = Vec::new();
821 loop {
822 match tokio::time::timeout(Duration::from_secs(2), interp.events.recv()).await {
823 Ok(Some(ev)) => {
824 if render_console {
825 renderer.render(&ev);
826 }
827 let done = matches!(ev, InterpreterOutputEvent::Ended(_));
828 events.push(ev);
829 if done {
830 break;
831 }
832 }
833 Ok(None) => break,
834 Err(_) => break,
835 }
836 }
837
838 let html = build_html_report(
839 &events,
840 &HtmlReportParams {
841 title: format!("multi-session {session_id}"),
842 show_tool_payloads: false,
843 ..Default::default()
844 },
845 );
846
847 Ok(LiveGrokSessionOutcome {
848 session_id,
849 prompt_result,
850 timed_out,
851 events,
852 chat_text: html.chat_projection.plain_text,
853 })
854}
855
856fn event_text(ev: &InterpreterOutputEvent) -> String {
857 match ev {
858 InterpreterOutputEvent::Unit(u) => match &u.snapshot().unit {
859 CanonicalUnit::Text(t) => t.content.clone(),
860 _ => String::new(),
861 },
862 _ => String::new(),
863 }
864}