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