monoloop_testkit/
live_agy.rs1use crate::console::{ConsoleRenderer, ConsoleRendererConfig, SyncMemorySink};
7use crate::html_report::{build_html_report, write_html_report, HtmlReport, HtmlReportParams};
8use monoloop_connector_agy::{AgyAgentConfig, AgyAgentHandle, AgySessionConfig};
9use monoloop_contracts::{
10 CanonicalUnit, DialectBinding, DialectDescriptor, InterpretationId, InterpretationLimits,
11 InterpreterOutputEvent,
12};
13use monoloop_interpreter::{DefaultInterpreterFactory, InterpreterFactory, StartInterpretation};
14use std::path::PathBuf;
15use std::time::Duration;
16
17#[derive(Clone, Debug)]
19pub struct LiveAgyRunOptions {
20 pub prompt: String,
22 pub cwd: PathBuf,
24 pub agent: AgyAgentConfig,
26 pub session: AgySessionConfig,
28 pub title: String,
30 pub artifact_stem: PathBuf,
32 pub render_console: bool,
34 pub drain_after_prompt: Duration,
36}
37
38impl LiveAgyRunOptions {
39 pub fn for_project(project: impl Into<PathBuf>, prompt: impl Into<String>) -> Self {
41 let project = project.into();
42 let stem = project.join("target/live_agy_run");
43 let mut agent = AgyAgentConfig::for_project(project.clone());
44 agent.raw_dump_path = Some(PathBuf::from(format!("{}.raw.txt", stem.display())));
45 agent.rpc_deadline = Duration::from_secs(10 * 60);
46 agent = agent.with_auto_allow_permissions();
47 agent.authenticate = false;
48 Self {
49 prompt: prompt.into(),
50 cwd: project.clone(),
51 agent,
52 session: AgySessionConfig::new(project),
53 title: "Live Antigravity ACP — interpretation review".into(),
54 artifact_stem: stem,
55 render_console: true,
56 drain_after_prompt: Duration::from_millis(300),
57 }
58 }
59}
60
61#[derive(Clone, Debug)]
63pub struct LiveAgyArtifactPaths {
64 pub html: PathBuf,
66 pub raw: PathBuf,
68 pub sequence: PathBuf,
70 pub chat: PathBuf,
72}
73
74#[derive(Clone, Debug)]
76pub struct LiveAgyRunReport {
77 pub session_id: String,
79 pub prompt_result: String,
81 pub events: Vec<InterpreterOutputEvent>,
83 pub html: HtmlReport,
85 pub console_text: String,
87 pub sequence_text: String,
89 pub paths: LiveAgyArtifactPaths,
91}
92
93pub async fn run_live_agy_prompt(opts: LiveAgyRunOptions) -> Result<LiveAgyRunReport, String> {
95 if let Some(parent) = opts.artifact_stem.parent() {
96 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
97 }
98
99 let mut agent = AgyAgentHandle::connect(opts.agent.clone())
100 .await
101 .map_err(|e| e.to_string())?;
102 let mut updates = agent.take_updates();
103 let mut session_cfg = opts.session.clone();
104 session_cfg.cwd = opts.cwd.clone();
105 let session = agent
106 .session_new(session_cfg)
107 .await
108 .map_err(|e| e.to_string())?;
109 let session_id = session.session_id.clone();
110
111 let dialect = DialectBinding::negotiated(DialectDescriptor::agy_acp("1"));
112 let factory = DefaultInterpreterFactory::new();
113 let interp = factory
114 .start(StartInterpretation {
115 interpretation_id: InterpretationId::generate(),
116 connection_id: monoloop_contracts::ConnectionId::new("agy-live"),
117 external_session_id: Some(session.external_session_id()),
118 dialect,
119 limits: InterpretationLimits::default(),
120 })
121 .map_err(|e| e.to_string())?;
122
123 let input = interp.input.clone();
124 let pump = tokio::spawn(async move {
125 while let Some(bytes) = updates.recv().await {
126 if input.push_bytes(bytes).await.is_err() {
127 break;
128 }
129 }
130 });
131
132 let prompt_result = session
133 .prompt_text(&opts.prompt)
134 .await
135 .map_err(|e| e.to_string())?;
136 let prompt_result_s = prompt_result.to_string();
137
138 tokio::time::sleep(opts.drain_after_prompt).await;
139 let dump_text = agent.raw_dump_text();
140 let _ = interp.input.finish_clean().await;
141 agent.shutdown().await;
142 let _ = pump.await;
143
144 let mut events = Vec::new();
145 let sink = std::sync::Arc::new(SyncMemorySink::new());
146 let console = ConsoleRenderer::new(ConsoleRendererConfig::default(), sink.clone());
147 loop {
148 match interp.events.recv().await {
149 Some(ev) => {
150 if opts.render_console {
151 console.render(&ev);
152 }
153 let done = matches!(ev, InterpreterOutputEvent::Ended(_));
154 events.push(ev);
155 if done {
156 break;
157 }
158 }
159 None => break,
160 }
161 }
162
163 let html = build_html_report(
164 &events,
165 &HtmlReportParams {
166 title: opts.title.clone(),
167 ..HtmlReportParams::default()
168 },
169 );
170 let html_path = PathBuf::from(format!("{}.html", opts.artifact_stem.display()));
171 write_html_report(&html_path, &html).map_err(|e| e.to_string())?;
172
173 let raw_path = PathBuf::from(format!("{}.raw.txt", opts.artifact_stem.display()));
174 if !dump_text.is_empty() {
175 let _ = std::fs::write(&raw_path, &dump_text);
176 } else if !raw_path.is_file() {
177 let _ = std::fs::write(&raw_path, "");
178 }
179
180 let mut sequence_text = String::from("=== LIVE AGY — CANONICAL TEXT ===\n");
181 for (i, e) in events.iter().enumerate() {
182 if let InterpreterOutputEvent::Unit(u) = e {
183 if let CanonicalUnit::Text(t) = &u.snapshot().unit {
184 sequence_text.push_str(&format!("{i:04} | {}\n", t.content));
185 }
186 }
187 }
188 sequence_text.push_str(&format!(
189 "\nsessionId={session_id}\nprompt_result={prompt_result_s}\n"
190 ));
191 let seq_path = PathBuf::from(format!("{}.sequence.txt", opts.artifact_stem.display()));
192 std::fs::write(&seq_path, &sequence_text).map_err(|e| e.to_string())?;
193
194 let chat_path = PathBuf::from(format!("{}.chat.txt", opts.artifact_stem.display()));
195 std::fs::write(&chat_path, &html.chat_projection.plain_text).map_err(|e| e.to_string())?;
196
197 Ok(LiveAgyRunReport {
198 session_id,
199 prompt_result: prompt_result_s,
200 events,
201 html,
202 console_text: sink.join(),
203 sequence_text,
204 paths: LiveAgyArtifactPaths {
205 html: html_path,
206 raw: raw_path,
207 sequence: seq_path,
208 chat: chat_path,
209 },
210 })
211}