Skip to main content

phi_agent/render/
json_stream.rs

1use std::io::{self, Write};
2
3use agent_base::{AgentResult, RuntimeEvent, UserEvent};
4use serde_json::{json, Value};
5
6use crate::render::EventRenderer;
7
8/// JSON stream renderer: outputs one JSON line per event (JSONL format).
9/// Suitable for IDE integrations and programmatic consumers.
10pub 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}