agentmux_srv/agents/translator/
gemini.rs1use serde_json::Value;
21
22use super::super::types::AgentEvent;
23
24#[derive(Default)]
25pub struct GeminiTranslator {
26 response: String,
27 terminal: bool,
28}
29
30impl GeminiTranslator {
31 pub fn new() -> Self {
32 Self::default()
33 }
34}
35
36impl super::Translator for GeminiTranslator {
37 fn translate(&mut self, frame: Value) -> Vec<AgentEvent> {
38 if self.terminal {
39 return Vec::new();
40 }
41 match frame.get("type").and_then(Value::as_str).unwrap_or("") {
42 "message" if frame.get("role").and_then(Value::as_str) == Some("assistant") => {
43 let chunk = frame.get("content").and_then(Value::as_str).unwrap_or("");
44 if chunk.is_empty() {
45 return Vec::new();
46 }
47 self.response.push_str(chunk);
48 vec![AgentEvent::AssistantText {
49 delta: chunk.to_string(),
50 }]
51 }
52 "result" => {
53 self.terminal = true;
54 let status = frame.get("status").and_then(Value::as_str).unwrap_or("success");
55 if status == "success" {
56 return vec![AgentEvent::Done {
57 response: std::mem::take(&mut self.response),
58 transcript: Vec::new(),
59 }];
60 }
61 let err = frame.get("error");
62 let message = err
63 .and_then(|e| e.get("message"))
64 .and_then(Value::as_str)
65 .or_else(|| err.and_then(Value::as_str))
66 .or_else(|| frame.get("message").and_then(Value::as_str))
67 .map(str::to_string)
68 .unwrap_or_else(|| format!("gemini turn {status}"));
69 vec![AgentEvent::Error { message }]
70 }
71 _ => Vec::new(),
72 }
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::super::Translator;
79 use super::*;
80 use serde_json::json;
81
82 #[test]
83 fn assistant_chunks_stream_and_a_successful_result_is_done() {
84 let mut t = GeminiTranslator::new();
85 let mut out = Vec::new();
86 for frame in [
87 json!({"type": "init", "session_id": "s1", "model": "gemini"}),
88 json!({"type": "message", "role": "user", "content": "why?"}),
89 json!({"type": "message", "role": "assistant", "content": "Because ", "delta": true}),
90 json!({"type": "message", "role": "assistant", "content": "reasons.", "delta": true}),
91 json!({"type": "result", "status": "success", "stats": {}}),
92 ] {
93 out.extend(t.translate(frame));
94 }
95 let deltas: Vec<_> = out
96 .iter()
97 .filter_map(|e| match e {
98 AgentEvent::AssistantText { delta } => Some(delta.as_str()),
99 _ => None,
100 })
101 .collect();
102 assert_eq!(deltas, ["Because ", "reasons."]);
103 match out.last() {
104 Some(AgentEvent::Done { response, .. }) => assert_eq!(response, "Because reasons."),
105 other => panic!("expected Done, got {other:?}"),
106 }
107 }
108
109 #[test]
110 fn a_failed_result_is_an_error_with_its_message() {
111 let mut t = GeminiTranslator::new();
112 let out = t.translate(json!({"type": "result", "status": "error", "error": {"type": "unknown", "message": "[API Error: quota]"}}));
113 match out.as_slice() {
114 [AgentEvent::Error { message }] => assert_eq!(message, "[API Error: quota]"),
115 other => panic!("expected one Error, got {other:?}"),
116 }
117 assert!(t.translate(json!({"type": "message", "role": "assistant", "content": "late"})).is_empty());
118 }
119
120 #[test]
121 fn tool_frames_are_dropped() {
122 let mut t = GeminiTranslator::new();
123 assert!(t.translate(json!({"type": "tool_use", "tool_name": "read_file", "tool_id": "t1"})).is_empty());
124 assert!(t.translate(json!({"type": "tool_result", "tool_id": "t1", "status": "success"})).is_empty());
125 }
126}