Skip to main content

atomr_agents_coding_cli_vendor_gemini/
parser.rs

1//! Gemini's stream-json shape — close to Claude's but with different
2//! envelope tags. Normalizes init / message / tool / result events.
3
4use atomr_agents_coding_cli_core::{
5    CliEventParser, CliVendorKind, CodingCliEvent, FinishReason, ParseError,
6};
7use serde_json::Value;
8
9#[derive(Default)]
10pub struct GeminiParser;
11
12impl GeminiParser {
13    pub fn new() -> Self {
14        Self
15    }
16}
17
18impl CliEventParser for GeminiParser {
19    fn parse_line(&mut self, line: &str) -> Result<Vec<CodingCliEvent>, ParseError> {
20        let s = line.trim();
21        if s.is_empty() {
22            return Ok(Vec::new());
23        }
24        let v: Value = serde_json::from_str(s)?;
25        Ok(normalize(&v))
26    }
27
28    fn flush(&mut self) -> Result<Vec<CodingCliEvent>, ParseError> {
29        Ok(Vec::new())
30    }
31}
32
33fn normalize(v: &Value) -> Vec<CodingCliEvent> {
34    match v.get("type").and_then(Value::as_str).unwrap_or("") {
35        "init" => vec![CodingCliEvent::SystemInit {
36            tools: Vec::new(),
37            mcp_servers: Vec::new(),
38            plugins: Vec::new(),
39        }],
40        "message" => {
41            if let Some(text) = v.pointer("/delta/text").and_then(Value::as_str) {
42                return vec![CodingCliEvent::AssistantTextDelta {
43                    text: text.to_string(),
44                }];
45            }
46            if let Some(text) = v.get("text").and_then(Value::as_str) {
47                return vec![CodingCliEvent::AssistantTextDelta {
48                    text: text.to_string(),
49                }];
50            }
51            vec![CodingCliEvent::RawVendorEvent {
52                vendor: CliVendorKind::Gemini,
53                payload: v.clone(),
54            }]
55        }
56        "tool_use" => {
57            let id = v.get("id").and_then(Value::as_str).unwrap_or("").to_string();
58            let name = v.get("name").and_then(Value::as_str).unwrap_or("").to_string();
59            let input = v.get("args").or_else(|| v.get("input")).cloned().unwrap_or(Value::Null);
60            vec![CodingCliEvent::ToolCallStarted {
61                tool_call_id: id,
62                name,
63                input,
64            }]
65        }
66        "tool_result" => {
67            let id = v
68                .get("tool_use_id")
69                .or_else(|| v.get("id"))
70                .and_then(Value::as_str)
71                .unwrap_or("")
72                .to_string();
73            let output = v.get("content").or_else(|| v.get("output")).cloned();
74            let error = v.get("error").and_then(Value::as_str).map(|s| s.to_string());
75            vec![CodingCliEvent::ToolCallFinished {
76                tool_call_id: id,
77                output,
78                error,
79            }]
80        }
81        "usage" => {
82            let input_tokens = v
83                .pointer("/stats/input_tokens")
84                .or_else(|| v.get("input_tokens"))
85                .and_then(Value::as_u64)
86                .unwrap_or(0);
87            let output_tokens = v
88                .pointer("/stats/output_tokens")
89                .or_else(|| v.get("output_tokens"))
90                .and_then(Value::as_u64)
91                .unwrap_or(0);
92            vec![CodingCliEvent::Usage {
93                input_tokens,
94                output_tokens,
95                cost_usd: None,
96            }]
97        }
98        "result" => {
99            let text = v
100                .get("response")
101                .and_then(Value::as_str)
102                .or_else(|| v.get("result").and_then(Value::as_str))
103                .map(|s| s.to_string());
104            vec![CodingCliEvent::RunFinished {
105                reason: FinishReason::Completed,
106                result_text: text,
107            }]
108        }
109        _ => vec![CodingCliEvent::RawVendorEvent {
110            vendor: CliVendorKind::Gemini,
111            payload: v.clone(),
112        }],
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn parses_message_delta() {
122        let mut p = GeminiParser::new();
123        let ev = p.parse_line(r#"{"type":"message","delta":{"text":"hi"}}"#).unwrap();
124        assert!(matches!(&ev[0], CodingCliEvent::AssistantTextDelta { text } if text == "hi"));
125    }
126
127    #[test]
128    fn parses_result_response() {
129        let mut p = GeminiParser::new();
130        let ev = p.parse_line(r#"{"type":"result","response":"done"}"#).unwrap();
131        assert!(matches!(
132            &ev[0],
133            CodingCliEvent::RunFinished {
134                reason: FinishReason::Completed,
135                result_text: Some(t),
136            } if t == "done"
137        ));
138    }
139
140    #[test]
141    fn parses_usage_stats() {
142        let mut p = GeminiParser::new();
143        let ev = p
144            .parse_line(r#"{"type":"usage","stats":{"input_tokens":10,"output_tokens":5}}"#)
145            .unwrap();
146        assert!(matches!(
147            &ev[0],
148            CodingCliEvent::Usage { input_tokens: 10, output_tokens: 5, cost_usd: None }
149        ));
150    }
151
152    #[test]
153    fn unknown_falls_through() {
154        let mut p = GeminiParser::new();
155        let ev = p.parse_line(r#"{"type":"weird","x":1}"#).unwrap();
156        assert!(matches!(&ev[0], CodingCliEvent::RawVendorEvent { .. }));
157    }
158}