Skip to main content

atomr_agents_coding_cli_vendor_antigravity/
parser.rs

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