supercode_interchange/session/
gemini.rs1use super::*;
4
5impl Session {
6 pub fn from_gemini(path: impl AsRef<Path>) -> Result<Session> {
8 Self::from_gemini_str(&std::fs::read_to_string(path.as_ref())?)
9 }
10
11 pub fn from_gemini_str(jsonl: &str) -> Result<Session> {
19 let mut meta = SessionMeta::new(SessionSource::Gemini);
20 let (raw_lines, raw_trailing_newline) = split_lines_verbatim(jsonl);
21 let raw = raw_lines.iter().map(|line| line.to_string()).collect();
22 let mut messages = Vec::new();
23 let mut parse_error_lines = 0usize;
24 let mut pending_by_name: HashMap<String, Vec<String>> = HashMap::new();
25
26 for (line_index, line) in non_empty_lines(jsonl).enumerate() {
27 let value: Value = match serde_json::from_str(line) {
28 Ok(value) => value,
29 Err(_) => {
30 parse_error_lines += 1;
31 continue;
32 }
33 };
34 let kind = value.get("type").and_then(Value::as_str);
35 if kind.is_none() {
36 if meta.session_id.is_none() {
37 meta.session_id = value
38 .get("sessionId")
39 .and_then(Value::as_str)
40 .map(str::to_string);
41 }
42 for (source, target) in [
43 ("projectHash", "gemini_project_hash"),
44 ("startTime", "created_at"),
45 ("lastUpdated", "updated_at"),
46 ("kind", "gemini_session_kind"),
47 ] {
48 if let Some(raw) = value.get(source) {
49 meta.lineage.insert(
50 target.to_string(),
51 raw.as_str()
52 .map(str::to_string)
53 .unwrap_or_else(|| raw.to_string()),
54 );
55 }
56 }
57 continue;
58 }
59 if kind != Some("user") && kind != Some("gemini") {
60 continue;
61 }
62
63 let timestamp = value.get("timestamp").and_then(Value::as_str);
64 let model = value.get("model").and_then(Value::as_str);
65 if let Some(model) = model {
66 meta.model = Some(model.to_string());
67 }
68 let content = value.get("content").unwrap_or(&Value::Null);
69 let parts = content.as_array();
70 let text = match content {
71 Value::String(text) => text.clone(),
72 Value::Array(parts) => parts
73 .iter()
74 .filter_map(|part| part.get("text").and_then(Value::as_str))
75 .collect::<Vec<_>>()
76 .join(" ")
77 .trim()
78 .to_string(),
79 _ => String::new(),
80 };
81
82 if kind == Some("gemini") {
83 let legacy_calls = parts
84 .into_iter()
85 .flatten()
86 .filter_map(|part| part.get("functionCall"));
87 let native_calls = value
88 .get("toolCalls")
89 .and_then(Value::as_array)
90 .into_iter()
91 .flatten();
92 let calls = native_calls
93 .chain(legacy_calls)
94 .enumerate()
95 .filter_map(|(call_index, call)| {
96 let name = call.get("name")?.as_str()?.to_string();
97 let id = call
98 .get("id")
99 .and_then(Value::as_str)
100 .map(str::to_string)
101 .unwrap_or_else(|| format!("gemini-{line_index}-{call_index}"));
102 pending_by_name
103 .entry(name.clone())
104 .or_default()
105 .push(id.clone());
106 let arguments = call
107 .get("args")
108 .map(value_to_arg_string)
109 .unwrap_or_else(|| "{}".to_string());
110 Some(function_call(&id, &name, arguments))
111 })
112 .collect::<Vec<_>>();
113 let mut message = ChatMessage {
114 role: Role::Assistant,
115 content: (!text.is_empty()).then_some(text),
116 content_parts: None,
117 tool_calls: (!calls.is_empty()).then_some(calls),
118 tool_call_id: None,
119 name: None,
120 metadata: Default::default(),
121 };
122 if let Some(timestamp) = timestamp {
123 message
124 .metadata
125 .insert("timestamp".into(), timestamp.into());
126 }
127 if let Some(model) = model {
128 message.metadata.insert("gemini_model".into(), model.into());
129 }
130 if let Some(thoughts) = value.get("thoughts").filter(|value| !value.is_null()) {
131 message
132 .metadata
133 .insert("gemini_thoughts".into(), thoughts.to_string());
134 }
135 restore_gemini_message_extension(&value, &mut message);
136 if message.content.is_some() || message.tool_calls.is_some() {
137 messages.push(message);
138 }
139 continue;
140 }
141
142 let mut user_parts = Vec::new();
143 if let Some(parts) = parts {
144 for part in parts {
145 if let Some(response) = part.get("functionResponse") {
146 push_gemini_user_parts(
147 &mut messages,
148 std::mem::take(&mut user_parts),
149 timestamp,
150 &value,
151 );
152 let name = response
153 .get("name")
154 .and_then(Value::as_str)
155 .unwrap_or("tool")
156 .to_string();
157 let explicit_id = response
158 .get("id")
159 .and_then(Value::as_str)
160 .map(str::to_string);
161 if let Some(id) = explicit_id.as_deref() {
162 if let Some(ids) = pending_by_name.get_mut(&name) {
163 if let Some(position) = ids.iter().position(|pending| pending == id)
164 {
165 ids.remove(position);
166 }
167 }
168 }
169 let id = explicit_id
170 .or_else(|| {
171 pending_by_name
172 .get_mut(&name)
173 .and_then(|ids| (!ids.is_empty()).then(|| ids.remove(0)))
174 })
175 .unwrap_or_else(|| format!("gemini-{line_index}-response"));
176 let output = response
177 .get("response")
178 .and_then(|response| response.get("output"))
179 .map(|output| {
180 output
181 .as_str()
182 .map(str::to_string)
183 .unwrap_or_else(|| output.to_string())
184 })
185 .or_else(|| response.get("response").map(Value::to_string))
186 .unwrap_or_default();
187 let mut message = tool_message(&id, output);
188 message.name = Some(name);
189 if let Some(timestamp) = timestamp {
190 message
191 .metadata
192 .insert("timestamp".into(), timestamp.into());
193 }
194 restore_gemini_message_extension(&value, &mut message);
195 messages.push(message);
196 continue;
197 }
198 if let Some(text) = part.get("text").and_then(Value::as_str) {
199 user_parts.push(serde_json::json!({"type": "text", "text": text}));
200 continue;
201 }
202 if let Some(inline) = part.get("inlineData") {
203 let Some(data) = inline.get("data").and_then(Value::as_str) else {
204 continue;
205 };
206 let media_type = inline
207 .get("mimeType")
208 .and_then(Value::as_str)
209 .unwrap_or("application/octet-stream");
210 user_parts.push(serde_json::json!({
211 "type": "image_url",
212 "image_url": {"url": format!("data:{media_type};base64,{data}")},
213 }));
214 }
215 }
216 } else if !text.is_empty() {
217 user_parts.push(serde_json::json!({"type": "text", "text": text}));
218 }
219 push_gemini_user_parts(&mut messages, user_parts, timestamp, &value);
220 }
221
222 ensure_tool_results_paired(&mut messages);
223 let imported_message_count = Some(messages.len());
224 Ok(Session {
225 meta,
226 messages,
227 subagents: Vec::new(),
228 raw,
229 raw_trailing_newline,
230 imported_message_count,
231 raw_is_verbatim: true,
232 parse_error_lines,
233 load_residue: Vec::new(),
234 })
235 }
236}
237
238const SUPERCODE_GEMINI_MESSAGE_KEY: &str = "_supercode_gemini_message";
239
240fn set_gemini_message_extension(value: &mut Value, message: &ChatMessage) {
241 value[SUPERCODE_GEMINI_MESSAGE_KEY] = serde_json::json!({
242 "schema": 1,
243 "role": message.role,
244 "content": message.content,
245 "content_parts": message.content_parts,
246 "tool_calls": message.tool_calls,
247 "tool_call_id": message.tool_call_id,
248 "name": message.name,
249 "metadata": message.metadata,
250 });
251}
252
253pub(super) fn restore_gemini_message_extension(value: &Value, message: &mut ChatMessage) {
254 let Some(extension) = value.get(SUPERCODE_GEMINI_MESSAGE_KEY) else {
255 return;
256 };
257 if extension.get("schema").and_then(Value::as_u64) != Some(1) {
258 return;
259 }
260 if let Some(role) = extension
261 .get("role")
262 .and_then(|value| serde_json::from_value(value.clone()).ok())
263 {
264 message.role = role;
265 }
266 message.content = extension
267 .get("content")
268 .and_then(Value::as_str)
269 .map(str::to_string);
270 message.content_parts = extension
271 .get("content_parts")
272 .and_then(|value| serde_json::from_value(value.clone()).ok());
273 message.tool_calls = extension
274 .get("tool_calls")
275 .and_then(|value| serde_json::from_value(value.clone()).ok());
276 message.tool_call_id = extension
277 .get("tool_call_id")
278 .and_then(Value::as_str)
279 .map(str::to_string);
280 message.name = extension
281 .get("name")
282 .and_then(Value::as_str)
283 .map(str::to_string);
284 message.metadata.clear();
285 if let Some(metadata) = extension.get("metadata").and_then(Value::as_object) {
286 for (key, value) in metadata {
287 if let Some(value) = value.as_str() {
288 message.metadata.insert(key.clone(), value.to_string());
289 }
290 }
291 }
292}
293
294impl Session {
295 pub(super) fn to_gemini_jsonl(&self) -> String {
298 let mut out = String::new();
299 self.write_gemini_header(&mut out, self.meta.session_id.as_deref());
300 self.write_gemini_records(&mut out, &self.messages);
301 push_jsonl(
302 &mut out,
303 &serde_json::json!({
304 "$set": {"lastUpdated": SYNTH_TS}
305 }),
306 );
307 out
308 }
309
310 fn write_gemini_header(&self, out: &mut String, session_id: Option<&str>) {
311 push_jsonl(
312 out,
313 &serde_json::json!({
314 "sessionId": session_id.unwrap_or("supercode-gemini-session"),
315 "projectHash": self.meta.lineage.get("gemini_project_hash")
316 .cloned().unwrap_or_else(|| "supercode".to_string()),
317 "startTime": self.meta.lineage.get("created_at")
318 .cloned().unwrap_or_else(|| SYNTH_TS.to_string()),
319 "lastUpdated": self.meta.lineage.get("updated_at")
320 .cloned().unwrap_or_else(|| SYNTH_TS.to_string()),
321 "kind": self.meta.lineage.get("gemini_session_kind")
322 .cloned().unwrap_or_else(|| "main".to_string()),
323 }),
324 );
325 }
326
327 fn write_gemini_records(&self, out: &mut String, messages: &[ChatMessage]) {
328 let mut call_names = HashMap::new();
329 for (index, message) in messages.iter().enumerate() {
330 if is_replay_excluded(message) {
331 continue;
332 }
333 let timestamp = message
334 .metadata
335 .get("timestamp")
336 .cloned()
337 .unwrap_or_else(|| SYNTH_TS.to_string());
338 match message.role {
339 Role::System | Role::User => {
340 let mut parts = Vec::new();
341 let text = message.content.clone().or_else(|| {
342 message.content_parts.as_ref().and_then(|parts| {
343 let text = parts
344 .iter()
345 .filter_map(|part| part.get("text").and_then(Value::as_str))
346 .collect::<Vec<_>>()
347 .join(" ");
348 (!text.is_empty()).then_some(text)
349 })
350 });
351 if let Some(text) = text {
352 let text = if message.role == Role::System {
353 format!("[System] {text}")
354 } else {
355 text
356 };
357 parts.push(serde_json::json!({"text": text}));
358 }
359 if let Some(content_parts) = &message.content_parts {
360 for part in content_parts {
361 let Some(url) = part
362 .get("image_url")
363 .and_then(|value| value.get("url"))
364 .and_then(Value::as_str)
365 else {
366 continue;
367 };
368 let Some(rest) = url.strip_prefix("data:") else {
369 continue;
370 };
371 let Some((media_type, data)) = rest.split_once(";base64,") else {
372 continue;
373 };
374 parts.push(serde_json::json!({
375 "inlineData": {"mimeType": media_type, "data": data}
376 }));
377 }
378 }
379 if !parts.is_empty() {
380 let mut value = serde_json::json!({
381 "id": format!("supercode-user-{index}"),
382 "timestamp": timestamp,
383 "type": "user",
384 "content": parts,
385 });
386 set_gemini_message_extension(&mut value, message);
387 push_jsonl(out, &value);
388 }
389 }
390 Role::Assistant => {
391 let mut tool_calls = Vec::new();
392 for call in message.tool_calls() {
393 call_names.insert(call.id.clone(), call.function.name.clone());
394 let args = serde_json::from_str::<Value>(&call.function.arguments)
395 .unwrap_or_else(|_| Value::String(call.function.arguments.clone()));
396 tool_calls.push(serde_json::json!({
397 "id": call.id,
398 "name": call.function.name,
399 "args": args,
400 }));
401 }
402 let mut value = serde_json::json!({
403 "id": format!("supercode-gemini-{index}"),
404 "timestamp": timestamp,
405 "type": "gemini",
406 "content": message.content.clone().unwrap_or_default(),
407 "model": message.metadata.get("gemini_model")
408 .or(self.meta.model.as_ref())
409 .cloned().unwrap_or_else(|| "unknown".to_string()),
410 });
411 if !tool_calls.is_empty() {
412 value["toolCalls"] = Value::Array(tool_calls);
413 }
414 if let Some(thoughts) = message.metadata.get("gemini_thoughts") {
415 value["thoughts"] = serde_json::from_str(thoughts)
416 .unwrap_or_else(|_| Value::String(thoughts.clone()));
417 }
418 set_gemini_message_extension(&mut value, message);
419 push_jsonl(out, &value);
420 }
421 Role::Tool => {
422 let id = message.tool_call_id.clone().unwrap_or_default();
423 let name = message
424 .name
425 .clone()
426 .or_else(|| call_names.get(&id).cloned())
427 .unwrap_or_else(|| "tool".to_string());
428 let output = message.content.clone().unwrap_or_else(|| {
429 message
430 .content_parts
431 .as_ref()
432 .map(|parts| Value::Array(parts.clone()))
433 .map(|value| value.to_string())
434 .unwrap_or_default()
435 });
436 let mut value = serde_json::json!({
437 "id": format!("supercode-tool-{index}"),
438 "timestamp": timestamp,
439 "type": "user",
440 "content": [{
441 "functionResponse": {
442 "id": id,
443 "name": name,
444 "response": {"output": output}
445 }
446 }],
447 });
448 set_gemini_message_extension(&mut value, message);
449 push_jsonl(out, &value);
450 }
451 }
452 }
453 }
454
455 pub(super) fn to_gemini_jsonl_spliced(&self, session_id: Option<&str>) -> String {
456 let (raw_prefix_len, message_prefix_len) = self.spliced_prefix_lens();
457 if raw_prefix_len == 0 {
458 let mut out = String::new();
459 self.write_gemini_header(&mut out, session_id.or(self.meta.session_id.as_deref()));
460 self.write_gemini_records(&mut out, &self.messages);
461 return out;
462 }
463 let mut out = String::new();
464 for (index, line) in self.raw[..raw_prefix_len].iter().enumerate() {
465 if index == 0 && session_id.is_some() {
466 if let Ok(mut value) = serde_json::from_str::<Value>(line) {
467 if value.get("type").is_none() && value.get("sessionId").is_some() {
468 value["sessionId"] =
469 Value::String(session_id.unwrap_or_default().to_string());
470 push_jsonl(&mut out, &value);
471 continue;
472 }
473 }
474 }
475 out.push_str(line);
476 out.push('\n');
477 }
478 self.write_gemini_records(&mut out, &self.messages[message_prefix_len..]);
479 out
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486
487 #[test]
488 fn gemini_user_parts_preserve_text_media_and_response_order() {
489 let session = Session::from_gemini_str(
490 r#"{"sessionId":"11111111-1111-4111-8111-111111111111","projectHash":"p"}
491{"type":"gemini","content":"calling","toolCalls":[{"id":"a","name":"read","args":{}},{"id":"b","name":"read","args":{}}]}
492{"type":"user","content":[{"text":"before"},{"functionResponse":{"id":"b","name":"read","response":{"output":"B"}}},{"inlineData":{"mimeType":"image/png","data":"YQ=="}},{"functionResponse":{"name":"read","response":{"output":"A"}}},{"text":"after"}]}
493"#,
494 )
495 .unwrap();
496
497 assert_eq!(session.messages.len(), 6);
498 assert_eq!(
499 session.messages[1].content_parts.as_ref().unwrap()[0]["text"],
500 "before"
501 );
502 assert_eq!(session.messages[2].tool_call_id.as_deref(), Some("b"));
503 assert!(
504 session.messages[3].content_parts.as_ref().unwrap()[0]["image_url"]["url"]
505 .as_str()
506 .unwrap()
507 .starts_with("data:image/png;base64,")
508 );
509 assert_eq!(session.messages[4].tool_call_id.as_deref(), Some("a"));
510 assert_eq!(
511 session.messages[5].content_parts.as_ref().unwrap()[0]["text"],
512 "after"
513 );
514 }
515}