1pub mod json_stream;
9pub mod null;
10pub mod terminal;
11
12pub use json_stream::JsonStreamRenderer;
13pub use null::NullRenderer;
14pub use terminal::TerminalRenderer;
15
16use std::io::{self, Write};
17
18use agent_base::{AgentResult, RuntimeEvent};
19
20pub trait EventRenderer: Send {
25 fn render(&mut self, event: RuntimeEvent) -> AgentResult<()>;
27
28 fn finish_turn(&mut self) -> AgentResult<()>;
30
31 fn finish_session(&mut self) -> AgentResult<()> {
33 Ok(())
34 }
35}
36
37#[derive(Clone, Debug)]
39pub enum OutputFormat {
40 Terminal {
42 show_thinking: bool,
44 show_tool_args: bool,
46 color: bool,
48 },
49 Json,
51 Quiet,
53}
54
55impl Default for OutputFormat {
56 fn default() -> Self {
58 OutputFormat::Terminal { show_thinking: false, show_tool_args: true, color: true }
59 }
60}
61
62pub fn create_renderer(format: &OutputFormat, writer: Option<Box<dyn Write + Send>>) -> Box<dyn EventRenderer> {
67 match format {
68 OutputFormat::Terminal { show_thinking, show_tool_args, color } => {
69 let w = writer.unwrap_or_else(|| Box::new(io::stdout()));
70 Box::new(TerminalRenderer::new(*show_thinking, *show_tool_args, *color, w))
71 },
72 OutputFormat::Json => {
73 let w = writer.unwrap_or_else(|| Box::new(io::stdout()));
74 Box::new(JsonStreamRenderer::new(w))
75 },
76 OutputFormat::Quiet => Box::new(NullRenderer),
77 }
78}
79
80pub fn create_stdout_renderer(format: &OutputFormat) -> Box<dyn EventRenderer> {
82 create_renderer(format, None)
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use agent_base::SessionId;
89 use std::io::Write;
90 use std::sync::{Arc, Mutex};
91
92 struct SharedWriter {
93 inner: Arc<Mutex<Vec<u8>>>,
94 }
95
96 impl Write for SharedWriter {
97 fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
98 self.inner.lock().unwrap().extend_from_slice(data);
99 Ok(data.len())
100 }
101 fn flush(&mut self) -> std::io::Result<()> {
102 Ok(())
103 }
104 }
105
106 fn session_id() -> SessionId {
107 SessionId { id: 1, external_id: None }
108 }
109
110 #[test]
111 fn test_create_terminal_renderer() {
112 let (writer, buf) = {
113 let inner = Arc::new(Mutex::new(Vec::new()));
114 (SharedWriter { inner: inner.clone() }, inner)
115 };
116 let format = OutputFormat::Terminal { show_thinking: true, show_tool_args: true, color: false };
117 let mut r = create_renderer(&format, Some(Box::new(writer)));
118 r.render(RuntimeEvent::TextDelta {
119 session_id: session_id(),
120 text: "hello".into(),
121 agent_id: None,
122 trace_id: None,
123 })
124 .unwrap();
125 drop(r);
126 let out = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
127 assert!(out.contains("hello"));
128 assert!(!out.contains('\x1b')); }
130
131 #[test]
132 fn test_create_json_renderer() {
133 let (writer, buf) = {
134 let inner = Arc::new(Mutex::new(Vec::new()));
135 (SharedWriter { inner: inner.clone() }, inner)
136 };
137 let mut r = create_renderer(&OutputFormat::Json, Some(Box::new(writer)));
138 r.render(RuntimeEvent::TextDelta {
139 session_id: session_id(),
140 text: "world".into(),
141 agent_id: None,
142 trace_id: None,
143 })
144 .unwrap();
145 drop(r);
146 let out = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
147 let v: serde_json::Value = serde_json::from_str(out.trim()).unwrap();
148 assert_eq!(v["type"], "text_delta");
149 }
150
151 #[test]
152 fn test_create_quiet_renderer() {
153 let (writer, buf) = {
154 let inner = Arc::new(Mutex::new(Vec::new()));
155 (SharedWriter { inner: inner.clone() }, inner)
156 };
157 let mut r = create_renderer(&OutputFormat::Quiet, Some(Box::new(writer)));
158 r.render(RuntimeEvent::TextDelta {
159 session_id: session_id(),
160 text: "quiet".into(),
161 agent_id: None,
162 trace_id: None,
163 })
164 .unwrap();
165 r.finish_turn().unwrap();
166 drop(r);
167 let out = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
168 assert!(out.is_empty());
169 }
170
171 #[test]
172 fn test_null_renderer_consumes_events_silently() {
173 let mut r = NullRenderer;
174 let sid = session_id();
175
176 assert!(
178 r.render(RuntimeEvent::TextDelta {
179 session_id: sid.clone(),
180 text: "hello".into(),
181 agent_id: None,
182 trace_id: None
183 })
184 .is_ok()
185 );
186 assert!(
187 r.render(RuntimeEvent::ThoughtDelta {
188 session_id: sid.clone(),
189 text: "thinking".into(),
190 agent_id: None,
191 trace_id: None
192 })
193 .is_ok()
194 );
195 assert!(
196 r.render(RuntimeEvent::ToolCallStarted {
197 session_id: sid.clone(),
198 tool_name: "test_tool".into(),
199 args_json: "{}".into(),
200 agent_id: None,
201 trace_id: None
202 })
203 .is_ok()
204 );
205 assert!(
206 r.render(RuntimeEvent::ToolCallFinished {
207 session_id: sid.clone(),
208 tool_name: "test_tool".into(),
209 summary: "done".into(),
210 agent_id: None,
211 trace_id: None,
212 denied: false,
213 })
214 .is_ok()
215 );
216 assert!(
217 r.render(RuntimeEvent::UserEvent {
218 session_id: sid.clone(),
219 event: agent_base::UserEvent::Progress { text: "progress".into() },
220 agent_id: None,
221 trace_id: None
222 })
223 .is_ok()
224 );
225 assert!(
226 r.render(RuntimeEvent::PlanUpdated {
227 session_id: sid.clone(),
228 objective: "obj".into(),
229 explanation: None,
230 plan: vec![],
231 agent_id: None,
232 trace_id: None
233 })
234 .is_ok()
235 );
236 assert!(
237 r.render(RuntimeEvent::RunFinished { session_id: sid.clone(), agent_id: None, trace_id: None }).is_ok()
238 );
239 assert!(r.finish_turn().is_ok());
240 assert!(r.finish_session().is_ok());
241 }
242}