1pub mod json_stream;
2pub mod null;
3pub mod terminal;
4
5pub use json_stream::JsonStreamRenderer;
6pub use null::NullRenderer;
7pub use terminal::TerminalRenderer;
8
9use std::io::{self, Write};
10
11use agent_base::{AgentResult, RuntimeEvent};
12
13pub trait EventRenderer: Send {
18 fn render(&mut self, event: RuntimeEvent) -> AgentResult<()>;
20
21 fn finish_turn(&mut self) -> AgentResult<()>;
23
24 fn finish_session(&mut self) -> AgentResult<()> {
26 Ok(())
27 }
28}
29
30#[derive(Clone, Debug)]
32pub enum OutputFormat {
33 Terminal { show_thinking: bool, show_tool_args: bool, color: bool },
35 Json,
37 Quiet,
39}
40
41pub fn create_renderer(format: &OutputFormat, writer: Option<Box<dyn Write + Send>>) -> Box<dyn EventRenderer> {
46 match format {
47 OutputFormat::Terminal { show_thinking, show_tool_args, color } => {
48 let w = writer.unwrap_or_else(|| Box::new(io::stdout()));
49 Box::new(TerminalRenderer::new(*show_thinking, *show_tool_args, *color, w))
50 },
51 OutputFormat::Json => {
52 let w = writer.unwrap_or_else(|| Box::new(io::stdout()));
53 Box::new(JsonStreamRenderer::new(w))
54 },
55 OutputFormat::Quiet => Box::new(NullRenderer),
56 }
57}
58
59pub fn create_stdout_renderer(format: &OutputFormat) -> Box<dyn EventRenderer> {
61 create_renderer(format, None)
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use agent_base::SessionId;
68 use std::io::Write;
69 use std::sync::{Arc, Mutex};
70
71 struct SharedWriter {
72 inner: Arc<Mutex<Vec<u8>>>,
73 }
74
75 impl Write for SharedWriter {
76 fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
77 self.inner.lock().unwrap().extend_from_slice(data);
78 Ok(data.len())
79 }
80 fn flush(&mut self) -> std::io::Result<()> {
81 Ok(())
82 }
83 }
84
85 fn session_id() -> SessionId {
86 SessionId { id: 1, external_id: None }
87 }
88
89 #[test]
90 fn test_create_terminal_renderer() {
91 let (writer, buf) = {
92 let inner = Arc::new(Mutex::new(Vec::new()));
93 (SharedWriter { inner: inner.clone() }, inner)
94 };
95 let format = OutputFormat::Terminal { show_thinking: true, show_tool_args: true, color: false };
96 let mut r = create_renderer(&format, Some(Box::new(writer)));
97 r.render(RuntimeEvent::TextDelta { session_id: session_id(), text: "hello".into() }).unwrap();
98 drop(r);
99 let out = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
100 assert!(out.contains("hello"));
101 assert!(!out.contains('\x1b')); }
103
104 #[test]
105 fn test_create_json_renderer() {
106 let (writer, buf) = {
107 let inner = Arc::new(Mutex::new(Vec::new()));
108 (SharedWriter { inner: inner.clone() }, inner)
109 };
110 let mut r = create_renderer(&OutputFormat::Json, Some(Box::new(writer)));
111 r.render(RuntimeEvent::TextDelta { session_id: session_id(), text: "world".into() }).unwrap();
112 drop(r);
113 let out = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
114 let v: serde_json::Value = serde_json::from_str(out.trim()).unwrap();
115 assert_eq!(v["type"], "text_delta");
116 }
117
118 #[test]
119 fn test_create_quiet_renderer() {
120 let (writer, buf) = {
121 let inner = Arc::new(Mutex::new(Vec::new()));
122 (SharedWriter { inner: inner.clone() }, inner)
123 };
124 let mut r = create_renderer(&OutputFormat::Quiet, Some(Box::new(writer)));
125 r.render(RuntimeEvent::TextDelta { session_id: session_id(), text: "quiet".into() }).unwrap();
126 r.finish_turn().unwrap();
127 drop(r);
128 let out = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
129 assert!(out.is_empty());
130 }
131}