1use std::collections::HashMap;
36use std::path::PathBuf;
37
38use chrono::{DateTime, Utc};
39use serde::{Deserialize, Serialize};
40use serde_json::{Map, Value, json};
41use uuid::Uuid;
42
43use crate::common::{Block, ImageSource, Message, Meta, Role, StopReason, Tool, ToolOutput, Usage};
44use crate::error::{Error, Result};
45use crate::transcript::{Codec, Common, Discovered, Harness, Saved, Store, TextCodec, Transcript};
46
47#[cfg(feature = "opencode")]
48use rusqlite::{Connection, OpenFlags, params};
49#[cfg(feature = "opencode")]
50use std::fs;
51#[cfg(feature = "opencode")]
52use std::path::Path;
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct Antigravity;
57
58impl Harness for Antigravity {
59 const NAME: &'static str = "antigravity";
60 type Body = AntigravityDb;
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66pub struct AntigravityDb {
67 pub trajectory_meta: Vec<TrajectoryMetaRow>,
68 pub steps: Vec<StepRow>,
69 #[serde(default, skip_serializing_if = "Vec::is_empty")]
70 pub gen_metadata: Vec<SizedBlobRow>,
71 #[serde(default, skip_serializing_if = "Vec::is_empty")]
72 pub executor_metadata: Vec<IndexedBlobRow>,
73 #[serde(default, skip_serializing_if = "Vec::is_empty")]
74 pub parent_references: Vec<IndexedBlobRow>,
75 #[serde(default, skip_serializing_if = "Vec::is_empty")]
76 pub battle_mode_infos: Vec<IndexedBlobRow>,
77 #[serde(default, skip_serializing_if = "Vec::is_empty")]
78 pub trajectory_metadata_blob: Vec<KeyedBlobRow>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub transcript: Option<String>,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub transcript_full: Option<String>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub struct TrajectoryMetaRow {
90 pub trajectory_id: String,
91 pub cascade_id: String,
92 pub trajectory_type: i64,
93 pub source: i64,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct StepRow {
100 pub idx: i64,
101 pub step_type: i64,
102 pub status: i64,
103 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
104 pub has_subtrajectory: bool,
105 #[serde(with = "serde_hex")]
106 pub metadata: Vec<u8>,
107 #[serde(with = "serde_hex", default, skip_serializing_if = "Vec::is_empty")]
108 pub error_details: Vec<u8>,
109 #[serde(with = "serde_hex", default, skip_serializing_if = "Vec::is_empty")]
110 pub permissions: Vec<u8>,
111 #[serde(with = "serde_hex", default, skip_serializing_if = "Vec::is_empty")]
112 pub task_details: Vec<u8>,
113 #[serde(with = "serde_hex", default, skip_serializing_if = "Vec::is_empty")]
114 pub render_info: Vec<u8>,
115 #[serde(with = "serde_hex")]
116 pub step_payload: Vec<u8>,
117 #[serde(default)]
118 pub step_format: i64,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub struct SizedBlobRow {
124 pub idx: i64,
125 #[serde(with = "serde_hex")]
126 pub data: Vec<u8>,
127 pub size: i64,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132pub struct IndexedBlobRow {
133 pub idx: i64,
134 #[serde(with = "serde_hex")]
135 pub data: Vec<u8>,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct KeyedBlobRow {
141 pub id: String,
142 #[serde(with = "serde_hex")]
143 pub data: Vec<u8>,
144}
145
146const STEP_VIEW_FILE: i64 = 8;
149const STEP_LIST_DIRECTORY: i64 = 9;
150const STEP_USER_INPUT: i64 = 14;
151const STEP_PLANNER_RESPONSE: i64 = 15;
152const STEP_RUN_COMMAND: i64 = 21;
153const STEP_CHECKPOINT: i64 = 23;
154const STEP_MCP_TOOL: i64 = 38;
155const STEP_CODE_ACTION: i64 = 5;
156const STEP_GREP_SEARCH: i64 = 7;
157const STEP_CONVERSATION_HISTORY: i64 = 98;
158const STEP_SYSTEM_MESSAGE: i64 = 101;
159const STEP_EPHEMERAL_MESSAGE: i64 = 90;
160const STEP_GENERIC: i64 = 132;
161
162const STATUS_PENDING: i64 = 1;
163const STATUS_DONE: i64 = 3;
164const STATUS_CANCELED: i64 = 6;
165const STATUS_ERROR: i64 = 7;
166
167fn payload_field(step_type: i64) -> Option<u32> {
169 match step_type {
170 STEP_VIEW_FILE => Some(14),
171 STEP_LIST_DIRECTORY => Some(15),
172 STEP_USER_INPUT => Some(19),
173 STEP_PLANNER_RESPONSE => Some(20),
174 STEP_RUN_COMMAND => Some(28),
175 STEP_CHECKPOINT => Some(30),
176 STEP_MCP_TOOL => Some(47),
177 STEP_CODE_ACTION => Some(10),
178 STEP_GREP_SEARCH => Some(13),
179 STEP_CONVERSATION_HISTORY => Some(111),
180 STEP_SYSTEM_MESSAGE => Some(114),
181 STEP_EPHEMERAL_MESSAGE => Some(103),
182 STEP_GENERIC => Some(140),
183 _ => None,
184 }
185}
186
187impl Codec for Antigravity {
188 fn to_common(transcript: &Transcript<Self>) -> Result<Transcript<Common>> {
189 Ok(Transcript::new(
190 transcript.meta.clone(),
191 db_to_messages(&transcript.body, &transcript.meta),
192 ))
193 }
194
195 fn from_common(transcript: &Transcript<Common>) -> Result<Transcript<Self>> {
196 Ok(Transcript::new(
197 transcript.meta.clone(),
198 db_from_messages(&transcript.meta, &transcript.body),
199 ))
200 }
201}
202
203impl TextCodec for Antigravity {
204 fn from_text(text: &str) -> Result<Transcript<Self>> {
205 let body: AntigravityDb = serde_json::from_str(text)?;
206 let meta = meta_from_db(&body);
207 Ok(Transcript::new(meta, body))
208 }
209
210 fn to_text(transcript: &Transcript<Self>) -> Result<String> {
211 Ok(serde_json::to_string_pretty(&transcript.body)?)
212 }
213}
214
215#[derive(Debug, Clone, PartialEq, Eq)]
219enum PbValue<'a> {
220 Varint(u64),
221 Bytes(&'a [u8]),
222 Fixed32([u8; 4]),
223 Fixed64([u8; 8]),
224}
225
226fn pb_fields(buf: &[u8]) -> Vec<(u32, PbValue<'_>)> {
230 let mut out = Vec::new();
231 let mut i = 0usize;
232 while i < buf.len() {
233 let Some((tag, next)) = pb_read_varint(buf, i) else {
234 break;
235 };
236 let field = u32::try_from(tag >> 3).unwrap_or(0);
237 let parsed = match tag & 7 {
238 0 => pb_read_varint(buf, next).map(|(v, j)| (PbValue::Varint(v), j)),
239 2 => pb_read_varint(buf, next).and_then(|(len, j)| {
240 let end = j.checked_add(usize::try_from(len).ok()?)?;
241 (end <= buf.len()).then(|| (PbValue::Bytes(&buf[j..end]), end))
242 }),
243 5 => buf
244 .get(next..next + 4)
245 .and_then(|b| <[u8; 4]>::try_from(b).ok())
246 .map(|b| (PbValue::Fixed32(b), next + 4)),
247 1 => buf
248 .get(next..next + 8)
249 .and_then(|b| <[u8; 8]>::try_from(b).ok())
250 .map(|b| (PbValue::Fixed64(b), next + 8)),
251 _ => None,
252 };
253 match parsed {
254 Some((value, j)) if field > 0 => {
255 out.push((field, value));
256 i = j;
257 }
258 Some(_) | None => i = buf.len(),
260 }
261 }
262 out
263}
264
265fn pb_read_varint(buf: &[u8], mut i: usize) -> Option<(u64, usize)> {
266 let mut value: u64 = 0;
267 let mut shift = 0u32;
268 while shift < 64 {
269 let byte = *buf.get(i)?;
270 value |= u64::from(byte & 0x7f) << shift;
271 i += 1;
272 if byte & 0x80 == 0 {
273 return Some((value, i));
274 }
275 shift += 7;
276 }
277 None
278}
279
280fn pb_sub<'a>(fields: &[(u32, PbValue<'a>)], field: u32) -> Option<&'a [u8]> {
282 fields.iter().find_map(|(f, v)| match v {
283 PbValue::Bytes(b) if *f == field => Some(*b),
284 _ => None,
285 })
286}
287
288fn pb_subs<'a>(fields: &[(u32, PbValue<'a>)], field: u32) -> Vec<&'a [u8]> {
290 fields
291 .iter()
292 .filter_map(|(f, v)| match v {
293 PbValue::Bytes(b) if *f == field => Some(*b),
294 _ => None,
295 })
296 .collect()
297}
298
299fn pb_str(fields: &[(u32, PbValue<'_>)], field: u32) -> Option<String> {
300 pb_sub(fields, field).and_then(|b| std::str::from_utf8(b).ok().map(String::from))
301}
302
303fn pb_uint(fields: &[(u32, PbValue<'_>)], field: u32) -> Option<u64> {
304 fields.iter().find_map(|(f, v)| match v {
305 PbValue::Varint(n) if *f == field => Some(*n),
306 _ => None,
307 })
308}
309
310fn pb_timestamp(buf: &[u8]) -> Option<DateTime<Utc>> {
312 let fields = pb_fields(buf);
313 let secs = i64::try_from(pb_uint(&fields, 1)?).ok()?;
314 let nanos = u32::try_from(pb_uint(&fields, 2).unwrap_or(0)).unwrap_or(0);
315 DateTime::from_timestamp(secs, nanos)
316}
317
318fn pb_key(out: &mut Vec<u8>, field: u32, wire_type: u8) {
319 pb_varint(out, (u64::from(field) << 3) | u64::from(wire_type));
320}
321
322#[allow(clippy::cast_possible_truncation)]
324fn pb_varint(out: &mut Vec<u8>, mut value: u64) {
325 while value >= 0x80 {
326 out.push((value as u8) | 0x80);
327 value >>= 7;
328 }
329 out.push(value as u8);
330}
331
332fn pb_varint_field(out: &mut Vec<u8>, field: u32, value: u64) {
333 pb_key(out, field, 0);
334 pb_varint(out, value);
335}
336
337fn pb_len(out: &mut Vec<u8>, field: u32, bytes: &[u8]) {
338 pb_key(out, field, 2);
339 pb_varint(out, bytes.len() as u64);
340 out.extend(bytes);
341}
342
343fn pb_string(out: &mut Vec<u8>, field: u32, value: &str) {
344 if !value.is_empty() {
345 pb_len(out, field, value.as_bytes());
346 }
347}
348
349fn pb_timestamp_field(out: &mut Vec<u8>, field: u32, ts: DateTime<Utc>) {
350 let mut inner = Vec::new();
351 let secs = ts.timestamp();
352 if let Ok(secs) = u64::try_from(secs)
353 && secs > 0
354 {
355 pb_varint_field(&mut inner, 1, secs);
356 }
357 let nanos = ts.timestamp_subsec_nanos();
358 if nanos > 0 {
359 pb_varint_field(&mut inner, 2, u64::from(nanos));
360 }
361 pb_len(out, field, &inner);
362}
363
364fn db_to_messages(db: &AntigravityDb, meta: &Meta) -> Vec<Message> {
367 let fallback_ts = meta.timestamp;
368 let mut steps: Vec<&StepRow> = db.steps.iter().collect();
369 steps.sort_by_key(|s| s.idx);
370
371 let mut messages = Vec::new();
372 for step in steps {
373 let payload = pb_fields(&step.step_payload);
374 let meta_fields = pb_sub(&payload, 5).map(pb_fields).unwrap_or_default();
375 let ts = pb_sub(&meta_fields, 1)
376 .and_then(pb_timestamp)
377 .unwrap_or(fallback_ts);
378 match step.step_type {
379 STEP_USER_INPUT => {
380 messages.extend(user_input_message(&payload, ts));
381 }
382 STEP_PLANNER_RESPONSE => {
383 messages.extend(planner_message(&payload, &meta_fields, ts));
384 }
385 STEP_CHECKPOINT
387 | STEP_CONVERSATION_HISTORY
388 | STEP_SYSTEM_MESSAGE
389 | STEP_EPHEMERAL_MESSAGE => {}
390 _ => {
391 messages.extend(tool_result_message(step, &payload, &meta_fields, ts));
392 }
393 }
394 }
395 messages
396}
397
398fn user_input_message(payload: &[(u32, PbValue<'_>)], ts: DateTime<Utc>) -> Option<Message> {
401 let input = pb_sub(payload, 19).map(pb_fields)?;
402 let text = pb_str(&input, 2)
403 .filter(|s| !s.trim().is_empty())
404 .or_else(|| {
405 pb_subs(&input, 3)
406 .into_iter()
407 .find_map(|item| pb_str(&pb_fields(item), 1).filter(|s| !s.trim().is_empty()))
408 })
409 .or_else(|| pb_str(&input, 1).filter(|s| !s.trim().is_empty()));
410
411 let mut content = Vec::new();
412 if let Some(text) = text {
413 content.push(Block::Text { text });
414 }
415 for image in pb_subs(&input, 5) {
416 let fields = pb_fields(image);
417 if let Some(data) = pb_str(&fields, 1) {
418 content.push(Block::Image {
419 source: ImageSource {
420 source_type: "base64".to_string(),
421 media_type: pb_str(&fields, 2).unwrap_or_else(|| "image/png".to_string()),
422 data,
423 },
424 });
425 }
426 }
427 (!content.is_empty()).then_some(Message {
428 role: Role::User,
429 content,
430 timestamp: ts,
431 model: None,
432 stop_reason: None,
433 usage: None,
434 })
435}
436
437fn planner_message(
440 payload: &[(u32, PbValue<'_>)],
441 meta_fields: &[(u32, PbValue<'_>)],
442 ts: DateTime<Utc>,
443) -> Option<Message> {
444 let planner = pb_sub(payload, 20).map(pb_fields).unwrap_or_default();
445
446 let mut content = Vec::new();
447 let thinking = pb_str(&planner, 3).unwrap_or_default();
448 if !thinking.trim().is_empty() {
449 content.push(Block::Thinking {
450 text: thinking,
451 signature: pb_str(&planner, 4).filter(|s| !s.is_empty()),
452 encrypted: None,
453 });
454 }
455 if let Some(text) = pb_str(&planner, 1).filter(|s| !s.trim().is_empty()) {
456 content.push(Block::Text { text });
457 }
458 for call in pb_subs(&planner, 7) {
459 content.extend(tool_use_block(call));
460 }
461
462 let usage = pb_sub(meta_fields, 9).map(pb_fields).and_then(|u| {
463 let input_tokens = pb_uint(&u, 2).unwrap_or(0);
464 let output_tokens = pb_uint(&u, 3).unwrap_or(0);
465 let cache_read = pb_uint(&u, 5);
466 let cache_write = pb_uint(&u, 4);
467 (input_tokens > 0 || output_tokens > 0).then_some(Usage {
468 input_tokens,
469 output_tokens,
470 cache_read_input_tokens: cache_read.filter(|n| *n > 0),
471 cache_creation_input_tokens: cache_write.filter(|n| *n > 0),
472 })
473 });
474
475 (!content.is_empty()).then(|| Message {
476 role: Role::Assistant,
477 content,
478 timestamp: ts,
479 model: pb_uint(meta_fields, 11).map(model_name),
480 stop_reason: pb_uint(&planner, 12).map(stop_reason_from_native),
481 usage,
482 })
483}
484
485fn tool_use_block(call: &[u8]) -> Option<Block> {
487 let fields = pb_fields(call);
488 let id = pb_str(&fields, 1)?;
489 let name = pb_str(&fields, 2)?;
490 let args = pb_str(&fields, 3)
491 .and_then(|s| serde_json::from_str::<Value>(&s).ok())
492 .unwrap_or_else(|| Value::Object(Map::new()));
493 Some(Block::ToolUse {
494 id,
495 tool: normalize_tool(&name, args),
496 })
497}
498
499fn tool_result_message(
501 step: &StepRow,
502 payload: &[(u32, PbValue<'_>)],
503 meta_fields: &[(u32, PbValue<'_>)],
504 ts: DateTime<Utc>,
505) -> Option<Message> {
506 let call = pb_sub(meta_fields, 4).map(pb_fields)?;
507 let tool_use_id = pb_str(&call, 1)?;
508 let done = matches!(step.status, STATUS_DONE | STATUS_ERROR | STATUS_CANCELED);
510 if !done {
511 return None;
512 }
513
514 let error_text = error_details_text(&step.error_details);
515 let (content, tool_error) = result_output(step.step_type, payload);
516 let is_error = step.status == STATUS_ERROR
517 || step.status == STATUS_CANCELED
518 || error_text.is_some()
519 || tool_error;
520 let content = match error_text {
521 Some(text) => ToolOutput::Text(text),
522 None => content,
523 };
524
525 Some(Message {
526 role: Role::User,
527 content: vec![Block::ToolResult {
528 tool_use_id,
529 content,
530 is_error,
531 }],
532 timestamp: ts,
533 model: None,
534 stop_reason: None,
535 usage: None,
536 })
537}
538
539fn error_details_text(error_details: &[u8]) -> Option<String> {
541 let fields = pb_fields(error_details);
542 pb_str(&fields, 9)
543 .or_else(|| pb_str(&fields, 1))
544 .or_else(|| pb_str(&fields, 2))
545 .filter(|s| !s.trim().is_empty())
546}
547
548fn result_output(step_type: i64, payload: &[(u32, PbValue<'_>)]) -> (ToolOutput, bool) {
551 let field = payload_field(step_type);
552 let body = field
553 .and_then(|f| pb_sub(payload, f))
554 .map(pb_fields)
555 .unwrap_or_default();
556 match step_type {
557 STEP_VIEW_FILE => {
559 let text = pb_str(&body, 4)
560 .or_else(|| pb_str(&body, 9))
561 .unwrap_or_default();
562 (ToolOutput::Text(text), false)
563 }
564 STEP_RUN_COMMAND => {
566 let text = pb_sub(&body, 21)
567 .and_then(|out| pb_str(&pb_fields(out), 1))
568 .or_else(|| pb_str(&body, 4))
569 .unwrap_or_default();
570 let exit = pb_uint(&body, 6).unwrap_or(0);
571 (ToolOutput::Text(text), exit != 0)
572 }
573 STEP_LIST_DIRECTORY => {
575 let entries: Vec<Value> = pb_subs(&body, 3)
576 .into_iter()
577 .filter_map(|entry| {
578 let fields = pb_fields(entry);
579 let name = pb_str(&fields, 1)?;
580 let mut obj = json!({ "name": name });
581 if pb_uint(&fields, 2) == Some(1) {
582 obj["is_dir"] = Value::Bool(true);
583 }
584 if let Some(size) = pb_uint(&fields, 4) {
585 obj["size_bytes"] = Value::from(size);
586 }
587 Some(obj)
588 })
589 .collect();
590 let path = pb_str(&body, 1)
591 .as_deref()
592 .map(strip_file_uri)
593 .unwrap_or_default();
594 (
595 ToolOutput::Json(json!({ "path": path, "entries": entries })),
596 false,
597 )
598 }
599 STEP_GREP_SEARCH => {
601 let error = pb_str(&body, 5).filter(|s| !s.trim().is_empty());
602 match error {
603 Some(text) => (ToolOutput::Text(text), true),
604 None => (
605 ToolOutput::Text(pb_str(&body, 3).unwrap_or_default()),
606 false,
607 ),
608 }
609 }
610 STEP_CODE_ACTION => {
613 let spec = pb_sub(&body, 1).map(pb_fields).unwrap_or_default();
614 let created = pb_sub(&spec, 2).is_some();
615 let uri = pb_sub(&body, 2)
616 .map(pb_fields)
617 .and_then(|result| {
618 pb_sub(&result, 1)
619 .map(pb_fields)
620 .and_then(|edit| pb_str(&edit, 8))
621 })
622 .or_else(|| spec_target_uri(&spec))
623 .unwrap_or_default();
624 let key = if created { "created" } else { "edited" };
625 (
626 ToolOutput::Json(json!({ "file": strip_file_uri(&uri), key: true })),
627 false,
628 )
629 }
630 STEP_MCP_TOOL => (
632 ToolOutput::Text(pb_str(&body, 3).unwrap_or_default()),
633 false,
634 ),
635 STEP_GENERIC => {
637 let text = pb_sub(&body, 2)
638 .and_then(|result| pb_str(&pb_fields(result), 1))
639 .unwrap_or_default();
640 (parse_result_text(text), false)
641 }
642 _ => (ToolOutput::Text(String::new()), false),
645 }
646}
647
648fn spec_target_uri(spec: &[(u32, PbValue<'_>)]) -> Option<String> {
650 let from_path_scope = |buf: &[u8]| pb_str(&pb_fields(buf), 5);
651 pb_sub(spec, 2)
652 .map(pb_fields)
653 .and_then(|create| pb_sub(&create, 2).and_then(from_path_scope))
654 .or_else(|| {
655 pb_sub(spec, 1)
656 .map(pb_fields)
657 .and_then(|command| pb_sub(&command, 4).and_then(from_path_scope))
658 })
659}
660
661fn parse_result_text(text: String) -> ToolOutput {
664 serde_json::from_str::<Value>(&text)
665 .ok()
666 .filter(|v| v.is_object() || v.is_array())
667 .map_or(ToolOutput::Text(text), ToolOutput::Json)
668}
669
670fn strip_file_uri(uri: &str) -> String {
671 uri.strip_prefix("file://").unwrap_or(uri).to_string()
672}
673
674fn file_uri(path: &str) -> String {
675 if path.starts_with("file://") {
676 path.to_string()
677 } else {
678 format!("file://{path}")
679 }
680}
681
682fn model_name(id: u64) -> String {
683 format!("model-{id}")
684}
685
686fn model_id(name: &str) -> Option<u64> {
687 name.strip_prefix("model-").and_then(|n| n.parse().ok())
688}
689
690fn stop_reason_from_native(value: u64) -> StopReason {
692 match value {
693 2 => StopReason::EndTurn,
694 3 => StopReason::MaxTokens,
695 10 => StopReason::ToolUse,
696 13 => StopReason::Error,
697 16 => StopReason::Aborted,
698 other => StopReason::Other(format!("stop-{other}")),
699 }
700}
701
702fn stop_reason_to_native(reason: &StopReason) -> Option<u64> {
703 match reason {
704 StopReason::EndTurn | StopReason::StopSequence => Some(2),
705 StopReason::MaxTokens => Some(3),
706 StopReason::ToolUse => Some(10),
707 StopReason::Error => Some(13),
708 StopReason::Aborted => Some(16),
709 StopReason::Other(s) => s.strip_prefix("stop-").and_then(|n| n.parse().ok()),
710 }
711}
712
713const DISPLAY_KEYS: [&str; 2] = ["toolAction", "toolSummary"];
718
719#[allow(clippy::too_many_lines)]
721fn normalize_tool(name: &str, args: Value) -> Tool {
722 let raw = || Tool::Raw {
723 tool_name: name.to_string(),
724 input: args.clone(),
725 };
726 match name {
727 "run_command" => typed_args(&args, |obj| {
728 let command = string_key(obj, "CommandLine")?;
729 let workdir = string_key(obj, "Cwd");
730 let mut extra = non_display_keys(obj);
731 extra.retain(|k| !matches!(k.as_str(), "CommandLine" | "Cwd"));
732 if obj
735 .get("WaitMsBeforeAsync")
736 .is_some_and(|v| v == &json!(2000))
737 {
738 extra.retain(|k| k != "WaitMsBeforeAsync");
739 }
740 let mut input = json!({ "command": command });
741 if let Some(workdir) = workdir {
742 input["workdir"] = Value::String(workdir);
743 }
744 extra
745 .is_empty()
746 .then(|| Tool::from_canonical("Bash", input))
747 })
748 .unwrap_or_else(raw),
749 "view_file" => typed_args(&args, |obj| {
750 let file_path = string_key(obj, "AbsolutePath")?;
751 let start = uint_key(obj, "StartLine");
752 let end = uint_key(obj, "EndLine");
753 let mut extra = non_display_keys(obj);
754 extra.retain(|k| !matches!(k.as_str(), "AbsolutePath" | "StartLine" | "EndLine"));
755 let limit = match (start, end) {
756 (Some(s), Some(e)) if e >= s => Some(e - s + 1),
757 (None, Some(e)) => Some(e),
758 (_, None) => None,
759 (Some(_), Some(_)) => {
761 extra.push(String::from("EndLine"));
762 None
763 }
764 };
765 let mut input = json!({ "file_path": file_path });
766 if let Some(offset) = start {
767 input["offset"] = Value::from(offset);
768 }
769 if let Some(limit) = limit {
770 input["limit"] = Value::from(limit);
771 }
772 extra
773 .is_empty()
774 .then(|| Tool::from_canonical("Read", input))
775 })
776 .unwrap_or_else(raw),
777 "write_to_file" => typed_args(&args, |obj| {
778 let file_path = string_key(obj, "TargetFile")?;
779 let content = string_key(obj, "CodeContent")?;
780 let mut extra = non_display_keys(obj);
781 extra.retain(|k| {
784 !matches!(
785 k.as_str(),
786 "TargetFile" | "CodeContent" | "Overwrite" | "Description" | "Instruction"
787 )
788 });
789 extra.is_empty().then(|| {
790 Tool::from_canonical(
791 "Write",
792 json!({ "file_path": file_path, "content": content }),
793 )
794 })
795 })
796 .unwrap_or_else(raw),
797 "replace_file_content" => typed_args(&args, |obj| {
798 let file_path = string_key(obj, "TargetFile")?;
799 let old_string = string_key(obj, "TargetContent")?;
800 let new_string = string_key(obj, "ReplacementContent")?;
801 let replace_all = obj
802 .get("AllowMultiple")
803 .and_then(Value::as_bool)
804 .unwrap_or(false);
805 let mut extra = non_display_keys(obj);
806 extra.retain(|k| {
807 !matches!(
808 k.as_str(),
809 "TargetFile"
810 | "TargetContent"
811 | "ReplacementContent"
812 | "AllowMultiple"
813 | "StartLine"
814 | "EndLine"
815 | "Description"
816 | "Instruction"
817 )
818 });
819 let mut input = json!({
820 "file_path": file_path,
821 "old_string": old_string,
822 "new_string": new_string,
823 });
824 if replace_all {
825 input["replace_all"] = Value::Bool(true);
826 }
827 extra
828 .is_empty()
829 .then(|| Tool::from_canonical("Edit", input))
830 })
831 .unwrap_or_else(raw),
832 other => Tool::from_canonical(other, args),
835 }
836}
837
838fn typed_args(args: &Value, f: impl FnOnce(&Map<String, Value>) -> Option<Tool>) -> Option<Tool> {
841 args.as_object().and_then(f)
842}
843
844fn string_key(obj: &Map<String, Value>, key: &str) -> Option<String> {
845 obj.get(key).and_then(Value::as_str).map(String::from)
846}
847
848fn uint_key(obj: &Map<String, Value>, key: &str) -> Option<u64> {
849 obj.get(key).and_then(Value::as_u64)
850}
851
852fn non_display_keys(obj: &Map<String, Value>) -> Vec<String> {
853 obj.keys()
854 .filter(|k| !DISPLAY_KEYS.contains(&k.as_str()))
855 .cloned()
856 .collect()
857}
858
859fn denormalize_tool(tool: &Tool) -> (String, Value) {
862 match tool {
863 Tool::Bash {
864 command, workdir, ..
865 } => {
866 let mut args = json!({
867 "CommandLine": command,
868 "WaitMsBeforeAsync": 2000,
869 });
870 if let Some(workdir) = workdir {
871 args["Cwd"] = Value::String(workdir.clone());
872 }
873 ("run_command".to_string(), args)
874 }
875 Tool::Read {
876 file_path,
877 offset,
878 limit,
879 } => {
880 let mut args = json!({ "AbsolutePath": file_path });
881 match (offset, limit) {
882 (Some(start), Some(len)) => {
883 args["StartLine"] = Value::from(*start);
884 args["EndLine"] = Value::from(start + len.saturating_sub(1));
885 }
886 (Some(start), None) => {
887 args["StartLine"] = Value::from(*start);
888 }
889 (None, Some(len)) => {
890 args["EndLine"] = Value::from(*len);
891 }
892 (None, None) => {}
893 }
894 ("view_file".to_string(), args)
895 }
896 Tool::Write { file_path, content } => (
897 "write_to_file".to_string(),
898 json!({
899 "TargetFile": file_path,
900 "CodeContent": content,
901 "Overwrite": true,
902 }),
903 ),
904 Tool::Edit {
905 file_path,
906 old_string,
907 new_string,
908 replace_all,
909 } => {
910 let mut args = json!({
911 "TargetFile": file_path,
912 "TargetContent": old_string,
913 "ReplacementContent": new_string,
914 });
915 if *replace_all {
916 args["AllowMultiple"] = Value::Bool(true);
917 }
918 ("replace_file_content".to_string(), args)
919 }
920 Tool::MultiEdit { .. } | Tool::Raw { .. } | Tool::Command { .. } => tool.to_canonical(),
923 }
924}
925
926const NS: Uuid = Uuid::from_bytes([
929 0x5e, 0x1f, 0x9a, 0x27, 0xb3, 0x44, 0x4c, 0x8e, 0x92, 0x6d, 0x0a, 0x71, 0xc5, 0x3f, 0x88, 0x14,
930]);
931
932#[derive(Debug, Clone)]
933struct PendingResult {
934 content: ToolOutput,
935 is_error: bool,
936 timestamp: DateTime<Utc>,
937}
938
939#[allow(clippy::too_many_lines)]
942fn db_from_messages(meta: &Meta, messages: &[Message]) -> AntigravityDb {
943 let session_id = if meta.id.is_empty() {
946 Uuid::new_v4().to_string()
947 } else {
948 meta.id.clone()
949 };
950 let trajectory_id =
951 Uuid::new_v5(&NS, format!("{session_id}:trajectory").as_bytes()).to_string();
952
953 let results: HashMap<String, PendingResult> = messages
954 .iter()
955 .flat_map(|m| m.content.iter().map(move |b| (m.timestamp, b)))
956 .filter_map(|(timestamp, block)| match block {
957 Block::ToolResult {
958 tool_use_id,
959 content,
960 is_error,
961 } => Some((
962 tool_use_id.clone(),
963 PendingResult {
964 content: content.clone(),
965 is_error: *is_error,
966 timestamp,
967 },
968 )),
969 Block::Text { .. }
971 | Block::Thinking { .. }
972 | Block::ToolUse { .. }
973 | Block::Image { .. } => None,
974 })
975 .collect();
976 let known_uses: std::collections::HashSet<String> = messages
977 .iter()
978 .flat_map(|m| m.content.iter())
979 .filter_map(|block| match block {
980 Block::ToolUse { id, .. } => Some(id.clone()),
981 Block::Text { .. }
983 | Block::Thinking { .. }
984 | Block::ToolResult { .. }
985 | Block::Image { .. } => None,
986 })
987 .collect();
988
989 let mut steps = Vec::new();
990 let mut turn: u64 = 0;
991 for message in messages {
992 match message.role {
993 Role::User => {
994 let orphan_text = orphan_result_text(&message.content, &known_uses);
995 let user_text = user_message_text(&message.content, &orphan_text);
996 let images = user_images(&message.content);
997 if !user_text.is_empty() || !images.is_empty() {
998 turn += 1;
999 steps.push(user_input_step(
1000 &session_id,
1001 &trajectory_id,
1002 steps.len() as u64,
1003 turn,
1004 &user_text,
1005 &images,
1006 message.timestamp,
1007 ));
1008 }
1009 }
1010 Role::Assistant => {
1011 let idx = steps.len() as u64;
1012 steps.push(planner_step(
1013 &session_id,
1014 &trajectory_id,
1015 idx,
1016 turn,
1017 message,
1018 ));
1019 for block in &message.content {
1020 if let Block::ToolUse { id, tool } = block {
1021 steps.push(tool_step(
1022 &session_id,
1023 &trajectory_id,
1024 steps.len() as u64,
1025 turn,
1026 id,
1027 tool,
1028 results.get(id),
1029 message.timestamp,
1030 ));
1031 }
1032 }
1033 }
1034 }
1035 }
1036
1037 let transcript = render_transcript(&steps, messages);
1038 AntigravityDb {
1039 trajectory_meta: vec![TrajectoryMetaRow {
1040 trajectory_id: trajectory_id.clone(),
1041 cascade_id: session_id.clone(),
1042 trajectory_type: 4,
1043 source: 17,
1044 }],
1045 steps,
1046 gen_metadata: Vec::new(),
1047 executor_metadata: Vec::new(),
1048 parent_references: Vec::new(),
1049 battle_mode_infos: Vec::new(),
1050 trajectory_metadata_blob: vec![KeyedBlobRow {
1051 id: "main".to_string(),
1052 data: trajectory_metadata_proto(meta, &session_id),
1053 }],
1054 transcript: Some(transcript.clone()),
1055 transcript_full: Some(transcript),
1056 }
1057}
1058
1059fn user_message_text(blocks: &[Block], orphan_text: &str) -> String {
1060 let mut parts: Vec<String> = blocks
1061 .iter()
1062 .filter_map(|block| match block {
1063 Block::Text { text } if !text.trim().is_empty() => Some(text.trim().to_string()),
1064 Block::Thinking { text, .. } if !text.trim().is_empty() => {
1065 Some(text.trim().to_string())
1066 }
1067 Block::Text { .. }
1069 | Block::Thinking { .. }
1070 | Block::ToolUse { .. }
1071 | Block::ToolResult { .. }
1072 | Block::Image { .. } => None,
1073 })
1074 .collect();
1075 if !orphan_text.is_empty() {
1076 parts.push(orphan_text.to_string());
1077 }
1078 parts.join("\n\n")
1079}
1080
1081fn user_images(blocks: &[Block]) -> Vec<&ImageSource> {
1082 blocks
1083 .iter()
1084 .filter_map(|block| match block {
1085 Block::Image { source } => Some(source),
1086 Block::Text { .. }
1088 | Block::Thinking { .. }
1089 | Block::ToolUse { .. }
1090 | Block::ToolResult { .. } => None,
1091 })
1092 .collect()
1093}
1094
1095fn orphan_result_text(blocks: &[Block], known_uses: &std::collections::HashSet<String>) -> String {
1098 blocks
1099 .iter()
1100 .filter_map(|block| match block {
1101 Block::ToolResult { tool_use_id, .. } if known_uses.contains(tool_use_id) => None,
1102 Block::ToolResult {
1103 tool_use_id,
1104 content,
1105 is_error,
1106 } => {
1107 let status = if *is_error { "error" } else { "result" };
1108 Some(format!(
1109 "Tool {status} for {tool_use_id}:\n{}",
1110 tool_output_text(content)
1111 ))
1112 }
1113 Block::Text { .. }
1115 | Block::Thinking { .. }
1116 | Block::ToolUse { .. }
1117 | Block::Image { .. } => None,
1118 })
1119 .collect::<Vec<_>>()
1120 .join("\n\n")
1121}
1122
1123fn tool_output_text(out: &ToolOutput) -> String {
1124 match out {
1125 ToolOutput::Text(s) => s.clone(),
1126 ToolOutput::Json(v) => v.to_string(),
1127 }
1128}
1129
1130struct StepParts {
1134 step_type: i64,
1135 status: i64,
1136 metadata: Vec<u8>,
1137 payload_field: u32,
1138 payload_body: Vec<u8>,
1139}
1140
1141fn assemble_step(idx: u64, parts: StepParts) -> StepRow {
1142 let mut step_payload = Vec::new();
1143 pb_varint_field(
1144 &mut step_payload,
1145 1,
1146 u64::try_from(parts.step_type).unwrap_or(0),
1147 );
1148 pb_varint_field(
1149 &mut step_payload,
1150 4,
1151 u64::try_from(parts.status).unwrap_or(3),
1152 );
1153 pb_len(&mut step_payload, 5, &parts.metadata);
1154 pb_len(&mut step_payload, parts.payload_field, &parts.payload_body);
1155 StepRow {
1156 idx: i64::try_from(idx).unwrap_or(0),
1157 step_type: parts.step_type,
1158 status: parts.status,
1159 has_subtrajectory: false,
1160 metadata: parts.metadata,
1161 error_details: Vec::new(),
1162 permissions: Vec::new(),
1163 task_details: Vec::new(),
1164 render_info: Vec::new(),
1165 step_payload,
1166 step_format: 0,
1167 }
1168}
1169
1170#[allow(clippy::too_many_arguments)]
1172fn step_metadata_proto(
1173 session_id: &str,
1174 trajectory_id: &str,
1175 idx: u64,
1176 turn: u64,
1177 source: u64,
1178 ts: DateTime<Utc>,
1179 tool_call: Option<&[u8]>,
1180 message: Option<&Message>,
1181) -> Vec<u8> {
1182 let mut out = Vec::new();
1183 pb_timestamp_field(&mut out, 1, ts);
1184 pb_varint_field(&mut out, 3, source);
1185 if let Some(call) = tool_call {
1186 pb_len(&mut out, 4, call);
1187 }
1188 if let Some(usage) = message.and_then(|m| m.usage.as_ref()) {
1189 let mut stats = Vec::new();
1190 if let Some(model) = message.and_then(|m| m.model.as_deref()).and_then(model_id) {
1191 pb_varint_field(&mut stats, 1, model);
1192 }
1193 pb_varint_field(&mut stats, 2, usage.input_tokens);
1194 pb_varint_field(&mut stats, 3, usage.output_tokens);
1195 if let Some(cache_write) = usage.cache_creation_input_tokens {
1196 pb_varint_field(&mut stats, 4, cache_write);
1197 }
1198 if let Some(cache_read) = usage.cache_read_input_tokens {
1199 pb_varint_field(&mut stats, 5, cache_read);
1200 }
1201 pb_len(&mut out, 9, &stats);
1202 }
1203 if let Some(model) = message.and_then(|m| m.model.as_deref()).and_then(model_id) {
1204 pb_varint_field(&mut out, 11, model);
1205 }
1206 let execution_id =
1207 Uuid::new_v5(&NS, format!("{session_id}:{turn}:exec").as_bytes()).to_string();
1208 pb_string(&mut out, 12, &execution_id);
1209 let mut step_info = Vec::new();
1210 pb_string(&mut step_info, 1, trajectory_id);
1211 if idx > 0 {
1212 pb_varint_field(&mut step_info, 2, idx);
1213 }
1214 if turn > 0 {
1215 pb_varint_field(&mut step_info, 3, turn.saturating_sub(1));
1216 }
1217 pb_string(&mut step_info, 4, session_id);
1218 pb_len(&mut out, 20, &step_info);
1219 pb_varint_field(&mut out, 21, 1);
1220 out
1221}
1222
1223fn user_input_step(
1224 session_id: &str,
1225 trajectory_id: &str,
1226 idx: u64,
1227 turn: u64,
1228 text: &str,
1229 images: &[&ImageSource],
1230 ts: DateTime<Utc>,
1231) -> StepRow {
1232 let mut body = Vec::new();
1233 pb_string(&mut body, 2, text);
1234 if !text.is_empty() {
1235 let mut item = Vec::new();
1236 pb_string(&mut item, 1, text);
1237 pb_len(&mut body, 3, &item);
1238 }
1239 for image in images {
1240 let mut data = Vec::new();
1241 pb_string(&mut data, 1, &image.data);
1242 pb_string(&mut data, 2, &image.media_type);
1243 pb_len(&mut body, 5, &data);
1244 }
1245 let metadata = step_metadata_proto(session_id, trajectory_id, idx, turn, 4, ts, None, None);
1246 assemble_step(
1247 idx,
1248 StepParts {
1249 step_type: STEP_USER_INPUT,
1250 status: STATUS_DONE,
1251 metadata,
1252 payload_field: 19,
1253 payload_body: body,
1254 },
1255 )
1256}
1257
1258fn planner_step(
1259 session_id: &str,
1260 trajectory_id: &str,
1261 idx: u64,
1262 turn: u64,
1263 message: &Message,
1264) -> StepRow {
1265 let mut body = Vec::new();
1266 let text = message
1267 .content
1268 .iter()
1269 .filter_map(|block| match block {
1270 Block::Text { text } if !text.trim().is_empty() => Some(text.trim().to_string()),
1271 Block::Image { source } => Some(format!("[image: {}]", source.media_type)),
1272 Block::Text { .. }
1275 | Block::Thinking { .. }
1276 | Block::ToolUse { .. }
1277 | Block::ToolResult { .. } => None,
1278 })
1279 .collect::<Vec<_>>()
1280 .join("\n\n");
1281 pb_string(&mut body, 1, &text);
1282 let thinking = message.content.iter().find_map(|block| match block {
1283 Block::Thinking {
1284 text, signature, ..
1285 } if !text.trim().is_empty() => Some((text.clone(), signature.clone())),
1286 Block::Text { .. }
1288 | Block::Thinking { .. }
1289 | Block::ToolUse { .. }
1290 | Block::ToolResult { .. }
1291 | Block::Image { .. } => None,
1292 });
1293 if let Some((thinking, signature)) = thinking {
1294 pb_string(&mut body, 3, &thinking);
1295 if let Some(signature) = signature {
1296 pb_string(&mut body, 4, &signature);
1297 }
1298 }
1299 let message_id = Uuid::new_v5(&NS, format!("{session_id}:{idx}:msg").as_bytes());
1300 pb_string(&mut body, 6, &format!("bot-{message_id}"));
1301 for block in &message.content {
1302 if let Block::ToolUse { id, tool } = block {
1303 pb_len(&mut body, 7, &chat_tool_call_proto(id, tool));
1304 }
1305 }
1306 pb_string(&mut body, 8, &text);
1307 if let Some(stop) = message.stop_reason.as_ref().and_then(stop_reason_to_native) {
1308 pb_varint_field(&mut body, 12, stop);
1309 }
1310 let metadata = step_metadata_proto(
1311 session_id,
1312 trajectory_id,
1313 idx,
1314 turn,
1315 2,
1316 message.timestamp,
1317 None,
1318 Some(message),
1319 );
1320 assemble_step(
1321 idx,
1322 StepParts {
1323 step_type: STEP_PLANNER_RESPONSE,
1324 status: STATUS_DONE,
1325 metadata,
1326 payload_field: 20,
1327 payload_body: body,
1328 },
1329 )
1330}
1331
1332fn chat_tool_call_proto(id: &str, tool: &Tool) -> Vec<u8> {
1335 let (name, args) = denormalize_tool(tool);
1336 let mut out = Vec::new();
1337 pb_string(&mut out, 1, id);
1338 pb_string(&mut out, 2, &name);
1339 pb_string(&mut out, 3, &args.to_string());
1340 pb_string(&mut out, 9, &name);
1341 out
1342}
1343
1344#[allow(clippy::too_many_arguments)]
1345fn tool_step(
1346 session_id: &str,
1347 trajectory_id: &str,
1348 idx: u64,
1349 turn: u64,
1350 call_id: &str,
1351 tool: &Tool,
1352 result: Option<&PendingResult>,
1353 call_ts: DateTime<Utc>,
1354) -> StepRow {
1355 let ts = result.map_or(call_ts, |r| r.timestamp);
1356 let call = chat_tool_call_proto(call_id, tool);
1357 let (step_type, payload_field_no, body, status) = tool_step_payload(tool, result);
1358 let metadata = step_metadata_proto(
1359 session_id,
1360 trajectory_id,
1361 idx,
1362 turn,
1363 2,
1364 ts,
1365 Some(&call),
1366 None,
1367 );
1368 assemble_step(
1369 idx,
1370 StepParts {
1371 step_type,
1372 status,
1373 metadata,
1374 payload_field: payload_field_no,
1375 payload_body: body,
1376 },
1377 )
1378}
1379
1380#[allow(clippy::too_many_lines)]
1385fn tool_step_payload(tool: &Tool, result: Option<&PendingResult>) -> (i64, u32, Vec<u8>, i64) {
1386 match tool {
1387 Tool::Read { .. } if result.is_some_and(|r| r.is_error) => generic_step_payload(result),
1391 Tool::Write { file_path, .. }
1392 if result.is_some_and(|r| {
1393 r.is_error || !is_derived_edit_result(r, file_path, "created")
1394 }) =>
1395 {
1396 generic_step_payload(result)
1397 }
1398 Tool::Edit { file_path, .. }
1399 if result
1400 .is_some_and(|r| r.is_error || !is_derived_edit_result(r, file_path, "edited")) =>
1401 {
1402 generic_step_payload(result)
1403 }
1404 Tool::Bash {
1405 command, workdir, ..
1406 } => {
1407 let mut body = Vec::new();
1408 pb_string(&mut body, 2, workdir.as_deref().unwrap_or_default());
1409 if let Some(result) = result {
1410 pb_varint_field(&mut body, 6, u64::from(result.is_error));
1411 }
1412 pb_varint_field(&mut body, 11, 1);
1413 pb_varint_field(&mut body, 12, 2000);
1414 if let Some(result) = result {
1415 let mut output = Vec::new();
1416 pb_string(&mut output, 1, &tool_output_text(&result.content));
1417 pb_len(&mut body, 21, &output);
1418 }
1419 pb_string(&mut body, 23, command);
1420 pb_string(&mut body, 25, command);
1421 let status = if result.is_some() {
1424 STATUS_DONE
1425 } else {
1426 STATUS_PENDING
1427 };
1428 (STEP_RUN_COMMAND, 28, body, status)
1429 }
1430 Tool::Read {
1431 file_path,
1432 offset,
1433 limit,
1434 } => {
1435 let mut body = Vec::new();
1436 pb_string(&mut body, 1, &file_uri(file_path));
1437 if let Some(start) = offset {
1438 pb_varint_field(&mut body, 2, *start);
1439 }
1440 match (offset, limit) {
1441 (Some(start), Some(len)) => {
1442 pb_varint_field(&mut body, 3, start + len.saturating_sub(1));
1443 }
1444 (None, Some(len)) => pb_varint_field(&mut body, 3, *len),
1445 (_, None) => {}
1446 }
1447 let status = match result {
1448 Some(r) => {
1450 let text = tool_output_text(&r.content);
1451 pb_string(&mut body, 4, &text);
1452 pb_varint_field(&mut body, 11, text.lines().count() as u64);
1453 pb_varint_field(&mut body, 12, text.len() as u64);
1454 STATUS_DONE
1455 }
1456 None => STATUS_PENDING,
1457 };
1458 (STEP_VIEW_FILE, 14, body, status)
1459 }
1460 Tool::Write { file_path, content } => {
1461 let uri = file_uri(file_path);
1462 let mut create = Vec::new();
1463 pb_string(&mut create, 1, content);
1464 let mut path = Vec::new();
1465 pb_string(&mut path, 5, &uri);
1466 pb_len(&mut create, 2, &path);
1467 pb_varint_field(&mut create, 4, 1);
1468 let mut spec = Vec::new();
1469 pb_len(&mut spec, 2, &create);
1470
1471 let mut body = Vec::new();
1472 pb_len(&mut body, 1, &spec);
1473 if result.is_some() {
1474 let mut edit = Vec::new();
1475 pb_string(&mut edit, 8, &uri);
1476 pb_varint_field(&mut edit, 11, 1);
1477 let mut action_result = Vec::new();
1478 pb_len(&mut action_result, 1, &edit);
1479 pb_len(&mut body, 2, &action_result);
1480 }
1481 pb_varint_field(&mut body, 4, 1);
1482 let status = if result.is_some() {
1484 STATUS_DONE
1485 } else {
1486 STATUS_PENDING
1487 };
1488 (STEP_CODE_ACTION, 10, body, status)
1489 }
1490 Tool::Edit {
1491 file_path,
1492 old_string,
1493 new_string,
1494 replace_all,
1495 } => {
1496 let uri = file_uri(file_path);
1497 let mut chunk = Vec::new();
1498 pb_string(&mut chunk, 1, old_string);
1499 pb_string(&mut chunk, 2, new_string);
1500 if *replace_all {
1501 pb_varint_field(&mut chunk, 3, 1);
1502 }
1503 let mut command = Vec::new();
1504 pb_varint_field(&mut command, 2, 1);
1505 let mut path = Vec::new();
1506 pb_string(&mut path, 5, &uri);
1507 pb_len(&mut command, 4, &path);
1508 pb_varint_field(&mut command, 8, 1);
1509 pb_len(&mut command, 9, &chunk);
1510 let mut spec = Vec::new();
1511 pb_len(&mut spec, 1, &command);
1512
1513 let mut body = Vec::new();
1514 pb_len(&mut body, 1, &spec);
1515 if result.is_some() {
1516 let mut edit = Vec::new();
1517 pb_string(&mut edit, 8, &uri);
1518 pb_string(&mut edit, 10, old_string);
1519 let mut action_result = Vec::new();
1520 pb_len(&mut action_result, 1, &edit);
1521 pb_len(&mut body, 2, &action_result);
1522 }
1523 pb_varint_field(&mut body, 4, 1);
1524 let status = if result.is_some() {
1526 STATUS_DONE
1527 } else {
1528 STATUS_PENDING
1529 };
1530 (STEP_CODE_ACTION, 10, body, status)
1531 }
1532 Tool::MultiEdit { .. } | Tool::Raw { .. } | Tool::Command { .. } => {
1533 generic_step_payload(result)
1534 }
1535 }
1536}
1537
1538fn generic_step_payload(result: Option<&PendingResult>) -> (i64, u32, Vec<u8>, i64) {
1541 let mut body = Vec::new();
1542 let status = match result {
1543 Some(r) => {
1544 let mut generic_result = Vec::new();
1545 pb_string(&mut generic_result, 1, &tool_output_text(&r.content));
1546 pb_len(&mut body, 2, &generic_result);
1547 if r.is_error {
1548 STATUS_ERROR
1549 } else {
1550 STATUS_DONE
1551 }
1552 }
1553 None => STATUS_PENDING,
1554 };
1555 (STEP_GENERIC, 140, body, status)
1556}
1557
1558fn is_derived_edit_result(result: &PendingResult, file_path: &str, key: &str) -> bool {
1562 matches!(&result.content, ToolOutput::Json(v) if *v == json!({ "file": file_path, key: true }))
1563}
1564
1565fn trajectory_metadata_proto(meta: &Meta, session_id: &str) -> Vec<u8> {
1567 let mut out = Vec::new();
1568 if let Some(cwd) = meta.cwd.as_deref() {
1569 let uri = file_uri(cwd);
1570 let mut workspace = Vec::new();
1571 pb_string(&mut workspace, 1, &uri);
1572 pb_string(&mut workspace, 2, &uri);
1573 if let Some(branch) = meta.git_branch.as_deref() {
1574 pb_string(&mut workspace, 4, branch);
1575 }
1576 pb_len(&mut out, 1, &workspace);
1577 pb_string(&mut out, 7, &uri);
1578 }
1579 pb_timestamp_field(&mut out, 2, meta.timestamp);
1580 pb_string(&mut out, 6, session_id);
1581 pb_string(&mut out, 18, "default-cli-project");
1582 out
1583}
1584
1585fn render_transcript(steps: &[StepRow], messages: &[Message]) -> String {
1588 let _ = messages;
1589 let mut out = String::new();
1590 for step in steps {
1591 let payload = pb_fields(&step.step_payload);
1592 let meta_fields = pb_sub(&payload, 5).map(pb_fields).unwrap_or_default();
1593 let ts = pb_sub(&meta_fields, 1).and_then(pb_timestamp);
1594 let (source, type_name) = match step.step_type {
1595 STEP_USER_INPUT => ("USER_EXPLICIT", "USER_INPUT"),
1596 STEP_PLANNER_RESPONSE => ("MODEL", "PLANNER_RESPONSE"),
1597 STEP_RUN_COMMAND => ("MODEL", "RUN_COMMAND"),
1598 STEP_VIEW_FILE => ("MODEL", "VIEW_FILE"),
1599 STEP_LIST_DIRECTORY => ("MODEL", "LIST_DIRECTORY"),
1600 STEP_CODE_ACTION => ("MODEL", "CODE_ACTION"),
1601 STEP_GREP_SEARCH => ("MODEL", "GREP_SEARCH"),
1602 STEP_MCP_TOOL => ("MODEL", "MCP_TOOL"),
1603 STEP_GENERIC => ("MODEL", "GENERIC"),
1604 _ => ("SYSTEM", "UNKNOWN"),
1605 };
1606 let mut line = json!({
1607 "step_index": step.idx,
1608 "source": source,
1609 "type": type_name,
1610 "status": if step.status == STATUS_ERROR { "ERROR" } else { "DONE" },
1611 });
1612 if let Some(ts) = ts {
1613 line["created_at"] = Value::String(ts.format("%Y-%m-%dT%H:%M:%SZ").to_string());
1614 }
1615 let content = match step.step_type {
1616 STEP_USER_INPUT => pb_sub(&payload, 19)
1617 .map(pb_fields)
1618 .and_then(|input| pb_str(&input, 2)),
1619 STEP_PLANNER_RESPONSE => pb_sub(&payload, 20)
1620 .map(pb_fields)
1621 .and_then(|planner| pb_str(&planner, 1)),
1622 _ => {
1623 let (output, _) = result_output(step.step_type, &payload);
1624 Some(tool_output_text(&output))
1625 }
1626 };
1627 if let Some(content) = content.filter(|c| !c.is_empty()) {
1628 line["content"] = Value::String(content);
1629 }
1630 out.push_str(&line.to_string());
1631 out.push('\n');
1632 }
1633 out
1634}
1635
1636fn meta_from_db(db: &AntigravityDb) -> Meta {
1639 let main_blob = db
1640 .trajectory_metadata_blob
1641 .iter()
1642 .find(|row| row.id == "main")
1643 .map(|row| pb_fields(&row.data))
1644 .unwrap_or_default();
1645
1646 let id = db
1647 .trajectory_meta
1648 .first()
1649 .map(|row| row.cascade_id.clone())
1650 .filter(|id| !id.is_empty())
1651 .or_else(|| pb_str(&main_blob, 6))
1652 .unwrap_or_default();
1653
1654 let workspace = pb_sub(&main_blob, 1).map(pb_fields).unwrap_or_default();
1655 let cwd = pb_str(&workspace, 1)
1656 .or_else(|| pb_str(&main_blob, 7))
1657 .as_deref()
1658 .map(strip_file_uri)
1659 .filter(|s| !s.is_empty());
1660 let git_branch = pb_str(&workspace, 4).filter(|s| !s.is_empty());
1661
1662 let mut steps: Vec<&StepRow> = db.steps.iter().collect();
1663 steps.sort_by_key(|s| s.idx);
1664
1665 let timestamp = pb_sub(&main_blob, 2)
1666 .and_then(pb_timestamp)
1667 .or_else(|| {
1668 steps.iter().find_map(|step| {
1669 let payload = pb_fields(&step.step_payload);
1670 let meta_fields = pb_sub(&payload, 5).map(pb_fields)?;
1671 pb_sub(&meta_fields, 1).and_then(pb_timestamp)
1672 })
1673 })
1674 .unwrap_or_else(|| DateTime::from_timestamp_millis(0).unwrap_or_else(Utc::now));
1675
1676 let title = steps
1679 .iter()
1680 .rev()
1681 .filter(|step| step.step_type == STEP_CHECKPOINT)
1682 .find_map(|step| {
1683 let payload = pb_fields(&step.step_payload);
1684 let checkpoint = pb_sub(&payload, 30).map(pb_fields)?;
1685 pb_str(&checkpoint, 10)
1686 .filter(|s| !s.trim().is_empty())
1687 .or_else(|| pb_str(&checkpoint, 4).filter(|s| !s.trim().is_empty()))
1688 })
1689 .or_else(|| {
1690 steps
1691 .iter()
1692 .filter(|step| step.step_type == STEP_USER_INPUT)
1693 .find_map(|step| {
1694 let payload = pb_fields(&step.step_payload);
1695 let input = pb_sub(&payload, 19).map(pb_fields)?;
1696 pb_str(&input, 2).filter(|s| !s.trim().is_empty())
1697 })
1698 .map(|text| truncate_title(&text))
1699 });
1700
1701 let model = steps
1702 .iter()
1703 .rev()
1704 .filter(|step| step.step_type == STEP_PLANNER_RESPONSE)
1705 .find_map(|step| {
1706 let payload = pb_fields(&step.step_payload);
1707 let meta_fields = pb_sub(&payload, 5).map(pb_fields)?;
1708 pb_uint(&meta_fields, 11).map(model_name)
1709 });
1710
1711 Meta {
1712 id,
1713 timestamp,
1714 cwd,
1715 git_branch,
1716 title,
1717 cli_version: None,
1718 model,
1719 }
1720}
1721
1722fn truncate_title(text: &str) -> String {
1723 const MAX: usize = 80;
1724 let one_line = text.split_whitespace().collect::<Vec<_>>().join(" ");
1725 if one_line.chars().count() <= MAX {
1726 one_line
1727 } else {
1728 let mut t: String = one_line.chars().take(MAX.saturating_sub(1)).collect();
1729 t.push('…');
1730 t
1731 }
1732}
1733
1734#[derive(Debug, Clone)]
1739pub struct AntigravityStore {
1740 pub root: PathBuf,
1741}
1742
1743impl AntigravityStore {
1744 pub fn new(root: impl Into<PathBuf>) -> Self {
1745 Self { root: root.into() }
1746 }
1747
1748 #[must_use]
1750 pub fn default_root() -> Option<Self> {
1751 super::home_dir().map(|home| Self::new(home.join(".gemini").join("antigravity-cli")))
1752 }
1753
1754 #[cfg(feature = "opencode")]
1755 fn conversations_dir(&self) -> PathBuf {
1756 self.root.join("conversations")
1757 }
1758
1759 #[cfg(feature = "opencode")]
1760 fn brain_dir(&self, id: &str) -> PathBuf {
1761 self.root.join("brain").join(id)
1762 }
1763}
1764
1765#[cfg(feature = "opencode")]
1766impl Store for AntigravityStore {
1767 type H = Antigravity;
1768 type Ref = PathBuf;
1769
1770 fn discover(&self) -> Result<Vec<Discovered<PathBuf>>> {
1771 Ok(fs::read_dir(self.conversations_dir())
1773 .into_iter()
1774 .flatten()
1775 .flatten()
1776 .map(|entry| entry.path())
1777 .filter(|path| path.extension().is_some_and(|ext| ext == "db"))
1778 .filter_map(|path| {
1779 let body = read_db(&path, &self.root).ok()?;
1781 let mut meta = meta_from_db(&body);
1782 backfill_meta(&mut meta, &path);
1783 Some(Discovered {
1784 meta,
1785 reference: path,
1786 })
1787 })
1788 .collect())
1789 }
1790
1791 fn load(&self, reference: &PathBuf) -> Result<Transcript<Antigravity>> {
1792 let body = read_db(reference, &self.root)?;
1793 let mut meta = meta_from_db(&body);
1794 backfill_meta(&mut meta, reference);
1795 Ok(Transcript::new(meta, body))
1796 }
1797
1798 fn save(&self, transcript: &Transcript<Antigravity>) -> Result<Saved<PathBuf>> {
1799 let id = [
1800 transcript.meta.id.clone(),
1801 transcript
1802 .body
1803 .trajectory_meta
1804 .first()
1805 .map(|row| row.cascade_id.clone())
1806 .unwrap_or_default(),
1807 ]
1808 .into_iter()
1809 .find(|candidate| !candidate.is_empty())
1810 .unwrap_or_else(|| Uuid::new_v4().to_string());
1811 super::checked_id_component(Antigravity::NAME, &id)?;
1812
1813 let conversations = self.conversations_dir();
1814 fs::create_dir_all(&conversations)?;
1815 let db_path = conversations.join(format!("{id}.db"));
1816 write_db(&db_path, &transcript.body)?;
1817
1818 let logs_dir = self.brain_dir(&id).join(".system_generated").join("logs");
1821 fs::create_dir_all(&logs_dir)?;
1822 if let Some(text) = transcript.body.transcript.as_deref() {
1823 fs::write(logs_dir.join("transcript.jsonl"), text)?;
1824 }
1825 if let Some(text) = transcript.body.transcript_full.as_deref() {
1826 fs::write(logs_dir.join("transcript_full.jsonl"), text)?;
1827 }
1828
1829 Ok(Saved {
1830 id,
1831 reference: db_path,
1832 })
1833 }
1834
1835 fn delete(&self, reference: &PathBuf) -> Result<()> {
1842 let id = reference
1843 .file_stem()
1844 .map(|s| s.to_string_lossy().into_owned())
1845 .filter(|_| reference.extension().is_some_and(|ext| ext == "db"))
1846 .ok_or_else(|| Error::Malformed {
1847 harness: Antigravity::NAME,
1848 detail: format!("not a conversation db path: {}", reference.display()),
1849 })?;
1850 super::checked_id_component(Antigravity::NAME, &id)?;
1851 let canon = reference.canonicalize()?;
1852 let conversations = self.conversations_dir().canonicalize()?;
1853 if canon.parent() != Some(conversations.as_path()) {
1854 return Err(Error::Malformed {
1855 harness: Antigravity::NAME,
1856 detail: format!(
1857 "refusing to delete outside the conversations root: {}",
1858 reference.display()
1859 ),
1860 });
1861 }
1862 fs::remove_file(&canon)?;
1863 for suffix in ["db-wal", "db-shm"] {
1864 let sidecar = canon.with_extension(suffix);
1865 if sidecar.exists() {
1866 fs::remove_file(sidecar)?;
1867 }
1868 }
1869 let brain = self.brain_dir(&id);
1870 if brain.is_dir() {
1871 fs::remove_dir_all(brain)?;
1872 }
1873 Ok(())
1874 }
1875
1876 fn fingerprints(&self, refs: &[PathBuf]) -> Result<HashMap<String, String>> {
1877 let mut out = HashMap::with_capacity(refs.len());
1878 for path in refs {
1879 out.insert(path.to_string_lossy().into_owned(), file_fingerprint(path));
1880 }
1881 Ok(out)
1882 }
1883}
1884
1885#[cfg(not(feature = "opencode"))]
1886impl Store for AntigravityStore {
1887 type H = Antigravity;
1888 type Ref = PathBuf;
1889
1890 fn discover(&self) -> Result<Vec<Discovered<PathBuf>>> {
1891 Ok(Vec::new())
1892 }
1893
1894 fn load(&self, _reference: &PathBuf) -> Result<Transcript<Antigravity>> {
1895 Err(sqlite_unavailable())
1896 }
1897
1898 fn save(&self, _transcript: &Transcript<Antigravity>) -> Result<Saved<PathBuf>> {
1899 Err(sqlite_unavailable())
1900 }
1901
1902 fn delete(&self, _reference: &PathBuf) -> Result<()> {
1903 Err(sqlite_unavailable())
1904 }
1905}
1906
1907#[cfg(not(feature = "opencode"))]
1908fn sqlite_unavailable() -> Error {
1909 Error::Unconvertible {
1910 harness: Antigravity::NAME,
1911 detail: "Antigravity store support requires the `opencode` feature for SQLite".to_string(),
1912 }
1913}
1914
1915#[cfg(feature = "opencode")]
1916fn backfill_meta(meta: &mut Meta, db_path: &Path) {
1917 if meta.id.is_empty() {
1918 meta.id = db_path
1919 .file_stem()
1920 .map(|s| s.to_string_lossy().into_owned())
1921 .unwrap_or_default();
1922 }
1923 if meta.timestamp.timestamp_millis() == 0 {
1924 meta.timestamp = fs::metadata(db_path)
1925 .ok()
1926 .and_then(|m| m.modified().ok())
1927 .map_or_else(Utc::now, DateTime::from);
1928 }
1929}
1930
1931#[allow(clippy::too_many_lines)]
1933#[cfg(feature = "opencode")]
1934fn read_db(db_path: &Path, root: &Path) -> Result<AntigravityDb> {
1935 let conn = Connection::open_with_flags(
1936 db_path,
1937 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
1938 )
1939 .map_err(sqlite_err)?;
1940
1941 let trajectory_meta = {
1942 let mut stmt = conn
1943 .prepare(
1944 "SELECT trajectory_id, cascade_id, trajectory_type, source \
1945 FROM trajectory_meta ORDER BY trajectory_id",
1946 )
1947 .map_err(sqlite_err)?;
1948 let rows = stmt
1949 .query_map([], |row| {
1950 Ok(TrajectoryMetaRow {
1951 trajectory_id: row.get(0)?,
1952 cascade_id: row.get(1)?,
1953 trajectory_type: row.get(2)?,
1954 source: row.get(3)?,
1955 })
1956 })
1957 .map_err(sqlite_err)?;
1958 rows.collect::<std::result::Result<Vec<_>, _>>()
1959 .map_err(sqlite_err)?
1960 };
1961
1962 let steps = {
1963 let mut stmt = conn
1964 .prepare(
1965 "SELECT idx, step_type, status, has_subtrajectory, metadata, error_details, \
1966 permissions, task_details, render_info, step_payload, step_format \
1967 FROM steps ORDER BY idx",
1968 )
1969 .map_err(sqlite_err)?;
1970 let rows = stmt
1971 .query_map([], |row| {
1972 Ok(StepRow {
1973 idx: row.get(0)?,
1974 step_type: row.get(1)?,
1975 status: row.get(2)?,
1976 has_subtrajectory: row.get::<_, Option<bool>>(3)?.unwrap_or(false),
1977 metadata: row.get::<_, Option<Vec<u8>>>(4)?.unwrap_or_default(),
1978 error_details: row.get::<_, Option<Vec<u8>>>(5)?.unwrap_or_default(),
1979 permissions: row.get::<_, Option<Vec<u8>>>(6)?.unwrap_or_default(),
1980 task_details: row.get::<_, Option<Vec<u8>>>(7)?.unwrap_or_default(),
1981 render_info: row.get::<_, Option<Vec<u8>>>(8)?.unwrap_or_default(),
1982 step_payload: row.get::<_, Option<Vec<u8>>>(9)?.unwrap_or_default(),
1983 step_format: row.get::<_, Option<i64>>(10)?.unwrap_or(0),
1984 })
1985 })
1986 .map_err(sqlite_err)?;
1987 rows.collect::<std::result::Result<Vec<_>, _>>()
1988 .map_err(sqlite_err)?
1989 };
1990
1991 let sized_rows = |table: &str| -> Result<Vec<SizedBlobRow>> {
1992 let mut stmt = conn
1993 .prepare(&format!("SELECT idx, data, size FROM {table} ORDER BY idx"))
1994 .map_err(sqlite_err)?;
1995 let rows = stmt
1996 .query_map([], |row| {
1997 Ok(SizedBlobRow {
1998 idx: row.get(0)?,
1999 data: row.get::<_, Option<Vec<u8>>>(1)?.unwrap_or_default(),
2000 size: row.get::<_, Option<i64>>(2)?.unwrap_or(0),
2001 })
2002 })
2003 .map_err(sqlite_err)?;
2004 rows.collect::<std::result::Result<Vec<_>, _>>()
2005 .map_err(sqlite_err)
2006 };
2007 let indexed_rows = |table: &str| -> Result<Vec<IndexedBlobRow>> {
2008 let mut stmt = conn
2009 .prepare(&format!("SELECT idx, data FROM {table} ORDER BY idx"))
2010 .map_err(sqlite_err)?;
2011 let rows = stmt
2012 .query_map([], |row| {
2013 Ok(IndexedBlobRow {
2014 idx: row.get(0)?,
2015 data: row.get::<_, Option<Vec<u8>>>(1)?.unwrap_or_default(),
2016 })
2017 })
2018 .map_err(sqlite_err)?;
2019 rows.collect::<std::result::Result<Vec<_>, _>>()
2020 .map_err(sqlite_err)
2021 };
2022
2023 let trajectory_metadata_blob = {
2024 let mut stmt = conn
2025 .prepare("SELECT id, data FROM trajectory_metadata_blob ORDER BY id")
2026 .map_err(sqlite_err)?;
2027 let rows = stmt
2028 .query_map([], |row| {
2029 Ok(KeyedBlobRow {
2030 id: row.get(0)?,
2031 data: row.get::<_, Option<Vec<u8>>>(1)?.unwrap_or_default(),
2032 })
2033 })
2034 .map_err(sqlite_err)?;
2035 rows.collect::<std::result::Result<Vec<_>, _>>()
2036 .map_err(sqlite_err)?
2037 };
2038
2039 let id = db_path
2040 .file_stem()
2041 .map(|s| s.to_string_lossy().into_owned())
2042 .unwrap_or_default();
2043 let logs = root
2044 .join("brain")
2045 .join(&id)
2046 .join(".system_generated")
2047 .join("logs");
2048 let transcript = fs::read_to_string(logs.join("transcript.jsonl")).ok();
2049 let transcript_full = fs::read_to_string(logs.join("transcript_full.jsonl")).ok();
2050
2051 Ok(AntigravityDb {
2052 trajectory_meta,
2053 steps,
2054 gen_metadata: sized_rows("gen_metadata")?,
2055 executor_metadata: indexed_rows("executor_metadata")?,
2056 parent_references: indexed_rows("parent_references")?,
2057 battle_mode_infos: indexed_rows("battle_mode_infos")?,
2058 trajectory_metadata_blob,
2059 transcript,
2060 transcript_full,
2061 })
2062}
2063
2064#[cfg(feature = "opencode")]
2065fn write_db(db_path: &Path, body: &AntigravityDb) -> Result<()> {
2066 let mut conn = Connection::open(db_path).map_err(sqlite_err)?;
2067 let tx = conn.transaction().map_err(sqlite_err)?;
2071 tx.execute_batch(
2072 "PRAGMA user_version = 1;\n\
2075 CREATE TABLE IF NOT EXISTS `trajectory_meta` (`trajectory_id` text,`cascade_id` text,\
2076 `trajectory_type` integer,`source` integer,PRIMARY KEY (`trajectory_id`));\n\
2077 CREATE TABLE IF NOT EXISTS `steps` (`idx` integer,`step_type` integer NOT NULL DEFAULT 0,\
2078 `status` integer NOT NULL DEFAULT 0,`has_subtrajectory` numeric NOT NULL DEFAULT false,\
2079 `metadata` blob,`error_details` blob,`permissions` blob,`task_details` blob,\
2080 `render_info` blob,`step_payload` blob,`step_format` integer NOT NULL DEFAULT 0,\
2081 PRIMARY KEY (`idx`));\n\
2082 CREATE INDEX IF NOT EXISTS `idx_steps_status` ON `steps`(`status`);\n\
2083 CREATE INDEX IF NOT EXISTS `idx_steps_step_type` ON `steps`(`step_type`);\n\
2084 CREATE TABLE IF NOT EXISTS `gen_metadata` (`idx` integer,`data` blob,\
2085 `size` integer NOT NULL DEFAULT 0,PRIMARY KEY (`idx`));\n\
2086 CREATE TABLE IF NOT EXISTS `executor_metadata` (`idx` integer,`data` blob,\
2087 PRIMARY KEY (`idx`));\n\
2088 CREATE TABLE IF NOT EXISTS `parent_references` (`idx` integer,`data` blob,\
2089 PRIMARY KEY (`idx`));\n\
2090 CREATE TABLE IF NOT EXISTS `trajectory_metadata_blob` (`id` text DEFAULT \"main\",\
2091 `data` blob,PRIMARY KEY (`id`));\n\
2092 CREATE TABLE IF NOT EXISTS `battle_mode_infos` (`idx` integer,`data` blob,\
2093 PRIMARY KEY (`idx`));\n\
2094 DELETE FROM trajectory_meta; DELETE FROM steps; DELETE FROM gen_metadata;\n\
2095 DELETE FROM executor_metadata; DELETE FROM parent_references;\n\
2096 DELETE FROM trajectory_metadata_blob; DELETE FROM battle_mode_infos;",
2097 )
2098 .map_err(sqlite_err)?;
2099
2100 for row in &body.trajectory_meta {
2101 tx.execute(
2102 "INSERT OR REPLACE INTO trajectory_meta \
2103 (trajectory_id, cascade_id, trajectory_type, source) VALUES (?1, ?2, ?3, ?4)",
2104 params![
2105 row.trajectory_id,
2106 row.cascade_id,
2107 row.trajectory_type,
2108 row.source
2109 ],
2110 )
2111 .map_err(sqlite_err)?;
2112 }
2113 for step in &body.steps {
2114 tx.execute(
2115 "INSERT OR REPLACE INTO steps (idx, step_type, status, has_subtrajectory, metadata, \
2116 error_details, permissions, task_details, render_info, step_payload, step_format) \
2117 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
2118 params![
2119 step.idx,
2120 step.step_type,
2121 step.status,
2122 step.has_subtrajectory,
2123 step.metadata,
2124 step.error_details,
2125 step.permissions,
2126 step.task_details,
2127 step.render_info,
2128 step.step_payload,
2129 step.step_format,
2130 ],
2131 )
2132 .map_err(sqlite_err)?;
2133 }
2134 for row in &body.gen_metadata {
2135 tx.execute(
2136 "INSERT OR REPLACE INTO gen_metadata (idx, data, size) VALUES (?1, ?2, ?3)",
2137 params![row.idx, row.data, row.size],
2138 )
2139 .map_err(sqlite_err)?;
2140 }
2141 let indexed = [
2142 ("executor_metadata", &body.executor_metadata),
2143 ("parent_references", &body.parent_references),
2144 ("battle_mode_infos", &body.battle_mode_infos),
2145 ];
2146 for (table, rows) in indexed {
2147 for row in rows {
2148 tx.execute(
2149 &format!("INSERT OR REPLACE INTO {table} (idx, data) VALUES (?1, ?2)"),
2150 params![row.idx, row.data],
2151 )
2152 .map_err(sqlite_err)?;
2153 }
2154 }
2155 for row in &body.trajectory_metadata_blob {
2156 tx.execute(
2157 "INSERT OR REPLACE INTO trajectory_metadata_blob (id, data) VALUES (?1, ?2)",
2158 params![row.id, row.data],
2159 )
2160 .map_err(sqlite_err)?;
2161 }
2162 tx.commit().map_err(sqlite_err)
2163}
2164
2165#[allow(clippy::needless_pass_by_value)]
2167#[cfg(feature = "opencode")]
2168fn sqlite_err(e: rusqlite::Error) -> Error {
2169 Error::Malformed {
2170 harness: Antigravity::NAME,
2171 detail: e.to_string(),
2172 }
2173}
2174
2175#[cfg(feature = "opencode")]
2176fn file_fingerprint(path: &Path) -> String {
2177 fs::metadata(path).map_or_else(
2179 |_| String::new(),
2180 |meta| {
2181 let mtime = meta
2182 .modified()
2183 .ok()
2184 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
2185 .map_or(0, |d| d.as_nanos());
2186 format!("{mtime}:{}", meta.len())
2187 },
2188 )
2189}
2190
2191fn hex_encode(bytes: &[u8]) -> String {
2194 const HEX: &[u8; 16] = b"0123456789abcdef";
2195 let mut out = String::with_capacity(bytes.len() * 2);
2196 for &byte in bytes {
2197 out.push(char::from(HEX[(byte >> 4) as usize]));
2198 out.push(char::from(HEX[(byte & 0x0f) as usize]));
2199 }
2200 out
2201}
2202
2203fn hex_decode(s: &str) -> std::result::Result<Vec<u8>, String> {
2204 let bytes = s.as_bytes();
2205 if bytes.len().is_multiple_of(2) {
2206 bytes
2207 .chunks_exact(2)
2208 .map(|pair| Ok((hex_value(pair[0])? << 4) | hex_value(pair[1])?))
2209 .collect()
2210 } else {
2211 Err("hex string has odd length".to_string())
2212 }
2213}
2214
2215fn hex_value(b: u8) -> std::result::Result<u8, String> {
2216 match b {
2217 b'0'..=b'9' => Ok(b - b'0'),
2218 b'a'..=b'f' => Ok(b - b'a' + 10),
2219 b'A'..=b'F' => Ok(b - b'A' + 10),
2220 _ => Err(format!("invalid hex byte `{}`", char::from(b))),
2221 }
2222}
2223
2224mod serde_hex {
2225 use serde::{Deserialize, Deserializer, Serializer, de};
2226
2227 pub fn serialize<S>(bytes: &[u8], serializer: S) -> std::result::Result<S::Ok, S::Error>
2228 where
2229 S: Serializer,
2230 {
2231 serializer.serialize_str(&super::hex_encode(bytes))
2232 }
2233
2234 pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result<Vec<u8>, D::Error>
2235 where
2236 D: Deserializer<'de>,
2237 {
2238 let s = String::deserialize(deserializer)?;
2239 super::hex_decode(&s).map_err(de::Error::custom)
2240 }
2241}