monoloop_testkit/
live_zai.rs1use crate::console::{ConsoleRenderer, ConsoleRendererConfig, SyncMemorySink};
6use crate::html_report::{build_html_report, write_html_report, HtmlReport, HtmlReportParams};
7use monoloop_connector_zai::{run_headless_prompt, ZaiAgentConfig};
8use monoloop_contracts::{
9 CanonicalUnit, DialectBinding, DialectDescriptor, InterpretationId, InterpretationLimits,
10 InterpreterOutputEvent,
11};
12use monoloop_interpreter::{DefaultInterpreterFactory, InterpreterFactory, StartInterpretation};
13use std::path::PathBuf;
14use std::time::Duration;
15use tokio::sync::mpsc;
16
17#[derive(Clone, Debug)]
19pub struct LiveZaiRunOptions {
20 pub prompt: String,
22 pub cwd: PathBuf,
24 pub agent: ZaiAgentConfig,
26 pub title: String,
28 pub artifact_stem: PathBuf,
30 pub render_console: bool,
32}
33
34impl LiveZaiRunOptions {
35 pub fn for_project(project: impl Into<PathBuf>, prompt: impl Into<String>) -> Self {
37 let project = project.into();
38 let stem = project.join("target/live_zai_run");
39 let mut agent = ZaiAgentConfig::for_project(project.clone());
40 agent.raw_dump_path = Some(PathBuf::from(format!("{}.raw.txt", stem.display())));
41 agent.run_deadline = Duration::from_secs(10 * 60);
42 Self {
43 prompt: prompt.into(),
44 cwd: project,
45 agent,
46 title: "Live Z.ai CLI — interpretation review".into(),
47 artifact_stem: stem,
48 render_console: true,
49 }
50 }
51}
52
53#[derive(Clone, Debug)]
55pub struct LiveZaiArtifactPaths {
56 pub html: PathBuf,
58 pub raw: PathBuf,
60 pub sequence: PathBuf,
62 pub chat: PathBuf,
64}
65
66#[derive(Clone, Debug)]
68pub struct LiveZaiRunReport {
69 pub session_id: String,
71 pub exit_code: Option<i32>,
73 pub events: Vec<InterpreterOutputEvent>,
75 pub html: HtmlReport,
77 pub console_text: String,
79 pub sequence_text: String,
81 pub paths: LiveZaiArtifactPaths,
83}
84
85pub async fn run_live_zai_prompt(opts: LiveZaiRunOptions) -> Result<LiveZaiRunReport, String> {
87 if let Some(parent) = opts.artifact_stem.parent() {
88 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
89 }
90
91 let mut agent = opts.agent.clone();
92 agent.cwd = opts.cwd.clone();
93 agent.raw_dump_path = Some(PathBuf::from(format!(
94 "{}.raw.txt",
95 opts.artifact_stem.display()
96 )));
97
98 let (tx, mut updates) = mpsc::channel(256);
99 let run = tokio::spawn({
100 let agent = agent.clone();
101 let prompt = opts.prompt.clone();
102 async move { run_headless_prompt(&agent, &prompt, tx).await }
103 });
104
105 let dialect = DialectBinding::negotiated(DialectDescriptor::zai_cli("1"));
106 let factory = DefaultInterpreterFactory::new();
107 let interp = factory
108 .start(StartInterpretation {
109 interpretation_id: InterpretationId::generate(),
110 connection_id: monoloop_contracts::ConnectionId::new("zai-live"),
111 external_session_id: None,
112 dialect,
113 limits: InterpretationLimits::default(),
114 })
115 .map_err(|e| e.to_string())?;
116
117 let input = interp.input.clone();
118 let pump = tokio::spawn(async move {
119 while let Some(bytes) = updates.recv().await {
120 if input.push_bytes(bytes).await.is_err() {
121 break;
122 }
123 }
124 });
125
126 let outcome = run
127 .await
128 .map_err(|e| e.to_string())?
129 .map_err(|e| e.to_string())?;
130 let _ = pump.await;
131 let _ = interp.input.finish_clean().await;
132
133 let mut events = Vec::new();
134 let sink = std::sync::Arc::new(SyncMemorySink::new());
135 let console = ConsoleRenderer::new(ConsoleRendererConfig::default(), sink.clone());
136 loop {
137 match interp.events.recv().await {
138 Some(ev) => {
139 if opts.render_console {
140 console.render(&ev);
141 }
142 let done = matches!(ev, InterpreterOutputEvent::Ended(_));
143 events.push(ev);
144 if done {
145 break;
146 }
147 }
148 None => break,
149 }
150 }
151
152 let html = build_html_report(
153 &events,
154 &HtmlReportParams {
155 title: opts.title.clone(),
156 ..HtmlReportParams::default()
157 },
158 );
159 let html_path = PathBuf::from(format!("{}.html", opts.artifact_stem.display()));
160 write_html_report(&html_path, &html).map_err(|e| e.to_string())?;
161
162 let raw_path = PathBuf::from(format!("{}.raw.txt", opts.artifact_stem.display()));
163 if !outcome.raw_dump_text.is_empty() {
164 let _ = std::fs::write(&raw_path, &outcome.raw_dump_text);
165 }
166
167 let mut sequence_text = String::from("=== LIVE ZAI — CANONICAL TEXT ===\n");
168 for (i, ev) in events.iter().enumerate() {
169 if let InterpreterOutputEvent::Unit(u) = ev {
170 if let CanonicalUnit::Text(t) = &u.snapshot().unit {
171 sequence_text.push_str(&format!("{i:04} | {}\n", t.content));
172 }
173 }
174 }
175 sequence_text.push_str(&format!(
176 "\nsession={} exit={:?} sentences={} strategy={:?} confidence={:?}\n",
177 outcome.session_id,
178 outcome.exit_code,
179 html.sentence_count,
180 html.chat_projection.strategy,
181 html.chat_projection.confidence
182 ));
183 let seq_path = PathBuf::from(format!("{}.sequence.txt", opts.artifact_stem.display()));
184 let _ = std::fs::write(&seq_path, &sequence_text);
185
186 let chat_path = PathBuf::from(format!("{}.chat.txt", opts.artifact_stem.display()));
187 let _ = std::fs::write(&chat_path, &html.chat_projection.plain_text);
188
189 Ok(LiveZaiRunReport {
190 session_id: outcome.session_id,
191 exit_code: outcome.exit_code,
192 events,
193 html,
194 console_text: sink.join(),
195 sequence_text,
196 paths: LiveZaiArtifactPaths {
197 html: html_path,
198 raw: raw_path,
199 sequence: seq_path,
200 chat: chat_path,
201 },
202 })
203}