phi_agent/render/
json_stream.rs1use std::io::{self, Write};
2
3use agent_base::{AgentResult, RuntimeEvent, UserEvent};
4use serde_json::{json, Value};
5
6use crate::render::EventRenderer;
7
8pub struct JsonStreamRenderer {
11 writer: Box<dyn Write + Send>,
12 turn_start: Option<std::time::Instant>,
13 tool_call_count: u32,
14 last_assistant_text: String,
15}
16
17impl JsonStreamRenderer {
18 pub fn new(writer: Box<dyn Write + Send>) -> Self {
19 Self {
20 writer,
21 turn_start: None,
22 tool_call_count: 0,
23 last_assistant_text: String::new(),
24 }
25 }
26
27 pub fn stdout() -> Self {
28 Self::new(Box::new(io::stdout()))
29 }
30
31 fn emit(&mut self, value: &Value) -> AgentResult<()> {
32 let line = serde_json::to_string(value)
33 .map_err(|e| agent_base::AgentError::internal(format!("JSON serialize error: {e}")))?;
34 writeln!(self.writer, "{}", line)
35 .map_err(|e| agent_base::AgentError::internal(format!("write error: {e}")))?;
36 Ok(())
37 }
38}
39
40impl EventRenderer for JsonStreamRenderer {
41 fn render(&mut self, event: RuntimeEvent) -> AgentResult<()> {
42 if self.turn_start.is_none() {
43 self.turn_start = Some(std::time::Instant::now());
44 }
45
46 match &event {
47 RuntimeEvent::ThoughtDelta { text, .. } => {
48 self.emit(&json!({ "type": "thought_delta", "text": text }))?;
49 }
50 RuntimeEvent::TextDelta { text, .. } => {
51 self.last_assistant_text.push_str(text);
52 self.emit(&json!({ "type": "text_delta", "text": text }))?;
53 }
54 RuntimeEvent::ToolCallStarted { tool_name, args_json, .. } => {
55 self.tool_call_count += 1;
56 let args: Value = serde_json::from_str(args_json).unwrap_or(Value::Null);
57 self.emit(&json!({
58 "type": "tool_call_started",
59 "tool": tool_name,
60 "args": args,
61 }))?;
62 }
63 RuntimeEvent::ToolCallFinished { tool_name, summary, .. } => {
64 self.emit(&json!({
65 "type": "tool_call_finished",
66 "tool": tool_name,
67 "summary": summary,
68 }))?;
69 }
70 RuntimeEvent::AwaitingApproval { request, .. } => {
71 self.emit(&json!({
72 "type": "approval_request",
73 "title": request.title,
74 "risk": format!("{:?}", request.risk_level),
75 "message": request.message,
76 }))?;
77 }
78 RuntimeEvent::PlanUpdated { explanation, plan, .. } => {
79 self.emit(&json!({
80 "type": "plan_updated",
81 "explanation": explanation,
82 "plan": plan,
83 }))?;
84 }
85 RuntimeEvent::UserEvent {
86 event: UserEvent::Structured { event_type, data },
87 ..
88 } => {
89 self.emit(&json!({
90 "type": "user_event",
91 "event_type": event_type,
92 "data": data,
93 }))?;
94 }
95 RuntimeEvent::UserEvent { .. } => {}
96 RuntimeEvent::Checkpoint { .. } => {}
97 RuntimeEvent::RunFinished { .. } => {}
98 RuntimeEvent::RunCancelled { .. } => {
99 self.emit(&json!({ "type": "run_cancelled" }))?;
100 }
101 }
102
103 Ok(())
104 }
105
106 fn finish_turn(&mut self) -> AgentResult<()> {
107 let duration_ms = self
108 .turn_start
109 .map(|s| s.elapsed().as_millis() as u64)
110 .unwrap_or(0);
111
112 self.emit(&json!({
113 "type": "turn_finished",
114 "duration_ms": duration_ms,
115 "tool_call_count": self.tool_call_count,
116 "assistant_text": self.last_assistant_text.trim(),
117 }))?;
118
119 self.turn_start = None;
120 self.tool_call_count = 0;
121 self.last_assistant_text.clear();
122
123 Ok(())
124 }
125}