1#![allow(missing_docs)]
2
3use std::io::{self, Write};
4
5use agent_base::{AgentResult, RuntimeEvent, UserEvent};
6use serde_json::{Value, json};
7
8use crate::render::EventRenderer;
9
10pub struct JsonStreamRenderer {
13 writer: Box<dyn Write + Send>,
14 turn_start: Option<std::time::Instant>,
15 tool_call_count: u32,
16 last_assistant_text: String,
17}
18
19impl JsonStreamRenderer {
20 pub fn new(writer: Box<dyn Write + Send>) -> Self {
22 Self { writer, turn_start: None, tool_call_count: 0, last_assistant_text: String::new() }
23 }
24
25 pub fn stdout() -> Self {
27 Self::new(Box::new(io::stdout()))
28 }
29
30 fn emit(&mut self, value: &Value) -> AgentResult<()> {
31 let line = serde_json::to_string(value)
32 .map_err(|e| agent_base::AgentError::internal(format!("JSON serialize error: {e}")))?;
33 writeln!(self.writer, "{}", line).map_err(|e| agent_base::AgentError::internal(format!("write error: {e}")))?;
34 Ok(())
35 }
36}
37
38impl EventRenderer for JsonStreamRenderer {
39 fn render(&mut self, event: RuntimeEvent) -> AgentResult<()> {
40 if self.turn_start.is_none() {
41 self.turn_start = Some(std::time::Instant::now());
42 }
43
44 match &event {
45 RuntimeEvent::ThoughtDelta { text, .. } => {
46 self.emit(&json!({ "type": "thought_delta", "text": text }))?;
47 },
48 RuntimeEvent::TextDelta { text, .. } => {
49 self.last_assistant_text.push_str(text);
50 self.emit(&json!({ "type": "text_delta", "text": text }))?;
51 },
52 RuntimeEvent::ToolCallStarted { tool_name, args_json, .. } => {
53 self.tool_call_count += 1;
54 let args: Value = serde_json::from_str(args_json).unwrap_or(Value::Null);
55 self.emit(&json!({
56 "type": "tool_call_started",
57 "tool": tool_name,
58 "args": args,
59 }))?;
60 },
61 RuntimeEvent::ToolCallFinished { tool_name, summary, .. } => {
62 self.emit(&json!({
63 "type": "tool_call_finished",
64 "tool": tool_name,
65 "summary": summary,
66 }))?;
67 },
68 RuntimeEvent::AwaitingApproval { request, .. } => {
69 self.emit(&json!({
70 "type": "approval_request",
71 "title": request.title,
72 "risk": format!("{:?}", request.risk_level),
73 "message": request.message,
74 }))?;
75 },
76 RuntimeEvent::PlanUpdated { explanation, plan, .. } => {
77 self.emit(&json!({
78 "type": "plan_updated",
79 "explanation": explanation,
80 "plan": plan,
81 }))?;
82 },
83 RuntimeEvent::UserEvent { event: UserEvent::Structured { event_type, data }, .. } => {
84 self.emit(&json!({
85 "type": "user_event",
86 "event_type": event_type,
87 "data": data,
88 }))?;
89 },
90 RuntimeEvent::UserEvent { event: UserEvent::SubAgentEvent { subagent, event: inner }, .. } => {
91 use crate::event_log::event_to_value;
92 let inner_value = event_to_value(inner);
93 self.emit(&json!({
94 "type": "subagent_event",
95 "subagent": subagent,
96 "event": inner_value,
97 }))?;
98 },
99 RuntimeEvent::UserEvent { .. } => {},
100 RuntimeEvent::Checkpoint { .. } => {},
101 RuntimeEvent::RunFinished { .. } => {},
102 RuntimeEvent::RunCancelled { .. } => {
103 self.emit(&json!({ "type": "run_cancelled" }))?;
104 },
105 }
106
107 Ok(())
108 }
109
110 fn finish_turn(&mut self) -> AgentResult<()> {
111 let duration_ms = self.turn_start.map(|s| s.elapsed().as_millis() as u64).unwrap_or(0);
112
113 self.emit(&json!({
114 "type": "turn_finished",
115 "duration_ms": duration_ms,
116 "tool_call_count": self.tool_call_count,
117 "assistant_text": self.last_assistant_text.trim(),
118 }))?;
119
120 self.turn_start = None;
121 self.tool_call_count = 0;
122 self.last_assistant_text.clear();
123
124 Ok(())
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use agent_base::{ApprovalRequest, PlanItem, PlanStepStatus, RiskLevel, SessionId, UserEvent};
132 use std::io::Write;
133 use std::sync::{Arc, Mutex};
134
135 struct SharedWriter {
136 inner: Arc<Mutex<Vec<u8>>>,
137 }
138
139 impl Write for SharedWriter {
140 fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
141 self.inner.lock().unwrap().extend_from_slice(data);
142 Ok(data.len())
143 }
144 fn flush(&mut self) -> std::io::Result<()> {
145 Ok(())
146 }
147 }
148
149 impl SharedWriter {
150 fn new() -> (Self, Arc<Mutex<Vec<u8>>>) {
151 let inner = Arc::new(Mutex::new(Vec::new()));
152 (Self { inner: inner.clone() }, inner)
153 }
154 }
155
156 fn session_id() -> SessionId {
157 SessionId { id: 1, external_id: None }
158 }
159
160 fn render_one(event: RuntimeEvent) -> Vec<String> {
161 let (writer, buf) = SharedWriter::new();
162 let mut r = JsonStreamRenderer::new(Box::new(writer));
163 r.render(event).unwrap();
164 drop(r);
165 let text = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
166 if text.is_empty() { vec![] } else { text.lines().map(|l| l.to_string()).collect() }
167 }
168
169 fn render_and_finish(events: &[RuntimeEvent]) -> Vec<String> {
170 let (writer, buf) = SharedWriter::new();
171 let mut r = JsonStreamRenderer::new(Box::new(writer));
172 for e in events {
173 r.render(e.clone()).unwrap();
174 }
175 r.finish_turn().unwrap();
176 drop(r);
177 let text = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
178 text.lines().map(|l| l.to_string()).collect()
179 }
180
181 #[test]
182 fn test_text_delta_produces_valid_json() {
183 let lines = render_one(RuntimeEvent::TextDelta {
184 session_id: session_id(),
185 text: "hello".into(),
186 agent_id: None,
187 trace_id: None,
188 });
189 assert_eq!(lines.len(), 1);
190 let v: serde_json::Value = serde_json::from_str(&lines[0]).unwrap();
191 assert_eq!(v["type"], "text_delta");
192 assert_eq!(v["text"], "hello");
193 }
194
195 #[test]
196 fn test_thought_delta_produces_valid_json() {
197 let lines = render_one(RuntimeEvent::ThoughtDelta {
198 session_id: session_id(),
199 text: "thinking...".into(),
200 agent_id: None,
201 trace_id: None,
202 });
203 let v: serde_json::Value = serde_json::from_str(&lines[0]).unwrap();
204 assert_eq!(v["type"], "thought_delta");
205 }
206
207 #[test]
208 fn test_tool_call_started_parses_args() {
209 let lines = render_one(RuntimeEvent::ToolCallStarted {
210 session_id: session_id(),
211 tool_name: "shell".into(),
212 args_json: r#"{"cmd":"ls"}"#.into(),
213 agent_id: None,
214 trace_id: None,
215 });
216 let v: serde_json::Value = serde_json::from_str(&lines[0]).unwrap();
217 assert_eq!(v["type"], "tool_call_started");
218 assert_eq!(v["tool"], "shell");
219 assert_eq!(v["args"]["cmd"], "ls");
220 }
221
222 #[test]
223 fn test_tool_call_finished_produces_valid_json() {
224 let lines = render_one(RuntimeEvent::ToolCallFinished {
225 session_id: session_id(),
226 tool_name: "shell".into(),
227 summary: "done".into(),
228 agent_id: None,
229 trace_id: None,
230 });
231 let v: serde_json::Value = serde_json::from_str(&lines[0]).unwrap();
232 assert_eq!(v["type"], "tool_call_finished");
233 assert_eq!(v["tool"], "shell");
234 assert_eq!(v["summary"], "done");
235 }
236
237 #[test]
238 fn test_awaiting_approval_produces_valid_json() {
239 let lines = render_one(RuntimeEvent::AwaitingApproval {
240 session_id: session_id(),
241 request: ApprovalRequest {
242 title: "Delete".into(),
243 message: "Dangerous".into(),
244 action_key: None,
245 risk_level: RiskLevel::Destructive,
246 raw: None,
247 },
248 agent_id: None,
249 trace_id: None,
250 });
251 let v: serde_json::Value = serde_json::from_str(&lines[0]).unwrap();
252 assert_eq!(v["type"], "approval_request");
253 assert_eq!(v["title"], "Delete");
254 }
255
256 #[test]
257 fn test_plan_updated_produces_valid_json() {
258 let lines = render_one(RuntimeEvent::PlanUpdated {
259 session_id: session_id(),
260 objective: "test".into(),
261 explanation: Some("step 1 done".into()),
262 plan: vec![PlanItem { step: "Step 1".into(), status: PlanStepStatus::Completed }],
263 agent_id: None,
264 trace_id: None,
265 });
266 let v: serde_json::Value = serde_json::from_str(&lines[0]).unwrap();
267 assert_eq!(v["type"], "plan_updated");
268 }
269
270 #[test]
271 fn test_user_event_structured() {
272 let lines = render_one(RuntimeEvent::UserEvent {
273 session_id: session_id(),
274 event: UserEvent::Structured { event_type: "custom".into(), data: serde_json::json!({"key": "value"}) },
275 agent_id: None,
276 trace_id: None,
277 });
278 let v: serde_json::Value = serde_json::from_str(&lines[0]).unwrap();
279 assert_eq!(v["type"], "user_event");
280 assert_eq!(v["event_type"], "custom");
281 assert_eq!(v["data"]["key"], "value");
282 }
283
284 #[test]
285 fn test_user_event_progress_ignored() {
286 let lines = render_one(RuntimeEvent::UserEvent {
287 session_id: session_id(),
288 event: UserEvent::Progress { text: "loading...".into() },
289 agent_id: None,
290 trace_id: None,
291 });
292 assert!(lines.is_empty());
293 }
294
295 #[test]
296 fn test_run_finished_no_output() {
297 let lines = render_one(RuntimeEvent::RunFinished { session_id: session_id(), agent_id: None, trace_id: None });
298 assert!(lines.is_empty());
299 }
300
301 #[test]
302 fn test_run_cancelled_produces_valid_json() {
303 let lines = render_one(RuntimeEvent::RunCancelled { session_id: session_id(), agent_id: None, trace_id: None });
304 let v: serde_json::Value = serde_json::from_str(&lines[0]).unwrap();
305 assert_eq!(v["type"], "run_cancelled");
306 }
307
308 #[test]
309 fn test_finish_turn_emits_summary() {
310 let lines = render_and_finish(&[RuntimeEvent::TextDelta {
311 session_id: session_id(),
312 text: "hello".into(),
313 agent_id: None,
314 trace_id: None,
315 }]);
316 let last: serde_json::Value = serde_json::from_str(lines.last().unwrap()).unwrap();
317 assert_eq!(last["type"], "turn_finished");
318 assert!(last["duration_ms"].as_u64().is_some());
319 assert_eq!(last["tool_call_count"], 0);
320 }
321
322 #[test]
323 fn test_tool_call_count_incremented() {
324 let lines = render_and_finish(&[
325 RuntimeEvent::ToolCallStarted {
326 session_id: session_id(),
327 tool_name: "a".into(),
328 args_json: "{}".into(),
329 agent_id: None,
330 trace_id: None,
331 },
332 RuntimeEvent::ToolCallStarted {
333 session_id: session_id(),
334 tool_name: "b".into(),
335 args_json: "{}".into(),
336 agent_id: None,
337 trace_id: None,
338 },
339 ]);
340 let last: serde_json::Value = serde_json::from_str(lines.last().unwrap()).unwrap();
341 assert_eq!(last["tool_call_count"], 2);
342 }
343
344 #[test]
345 fn test_assistant_text_accumulated() {
346 let lines = render_and_finish(&[
347 RuntimeEvent::TextDelta { session_id: session_id(), text: "Hello ".into(), agent_id: None, trace_id: None },
348 RuntimeEvent::TextDelta { session_id: session_id(), text: "World".into(), agent_id: None, trace_id: None },
349 ]);
350 let last: serde_json::Value = serde_json::from_str(lines.last().unwrap()).unwrap();
351 assert_eq!(last["assistant_text"], "Hello World");
352 }
353
354 #[test]
357 fn test_subagent_event_produces_structured_json() {
358 let lines = render_one(RuntimeEvent::UserEvent {
359 session_id: session_id(),
360 event: UserEvent::SubAgentEvent {
361 subagent: "root/searcher".into(),
362 event: Box::new(RuntimeEvent::TextDelta {
363 session_id: session_id(),
364 text: "found 3 items".into(),
365 agent_id: None,
366 trace_id: None,
367 }),
368 },
369 agent_id: None,
370 trace_id: None,
371 });
372 assert_eq!(lines.len(), 1);
373 let v: serde_json::Value = serde_json::from_str(&lines[0]).unwrap();
374 assert_eq!(v["type"], "subagent_event");
375 assert_eq!(v["subagent"], "root/searcher");
376 assert_eq!(v["event"]["type"], "text_delta");
377 assert_eq!(v["event"]["text"], "found 3 items");
378 }
379
380 #[test]
381 fn test_subagent_event_with_tool_call() {
382 let lines = render_one(RuntimeEvent::UserEvent {
383 session_id: session_id(),
384 event: UserEvent::SubAgentEvent {
385 subagent: "root/worker".into(),
386 event: Box::new(RuntimeEvent::ToolCallStarted {
387 session_id: session_id(),
388 tool_name: "shell".into(),
389 args_json: r#"{"cmd":"ls"}"#.into(),
390 agent_id: None,
391 trace_id: None,
392 }),
393 },
394 agent_id: None,
395 trace_id: None,
396 });
397 let v: serde_json::Value = serde_json::from_str(&lines[0]).unwrap();
398 assert_eq!(v["type"], "subagent_event");
399 assert_eq!(v["subagent"], "root/worker");
400 assert_eq!(v["event"]["type"], "tool_call_started");
401 assert_eq!(v["event"]["tool"], "shell");
402 }
403}