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