atomr_agents_coding_cli_vendor_gemini/
parser.rs1use 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}