Antigravity Brain Explorer

Interactive Code & Architecture Walkthrough

Skill Spec Brain Directory Spec v1.1 • CLI + Python 3.11+

Inside the Antigravity Brain Exploration Architecture

An annotated, syntax-highlighted walkthrough of agy-brain-explorer.py. Discover how Antigravity manages session trajectories, parses dual-mode JSONL transcripts, resolves execution lineage, indexes past sessions, and provides cross-session and intra-session forensic search across ~/.gemini/ brain stores.

18 SECTIONS
1,514 LINES OF CODE
2 CLI APPS
8 SUBCOMMANDS
PEP 723 UV EXECUTABLE
Infrastructure & Imports Lines 1–21

1. PEP 723 Inline Metadata & Core Imports

Every standalone executable script in modern Python faces a packaging dilemma: how to specify third-party dependencies without forcing the user to create a virtual environment, run pip install, or maintain separate configuration files like requirements.txt or pyproject.toml.

agy-brain-explorer.py solves this cleanly using PEP 723 (Inline Script Metadata). When run with uv (via uv run agy-brain-explorer.py), the tool automatically reads this comment block, constructs an isolated, cached environment with Python ≥ 3.11 and Typer ≥ 0.12.0, and executes instantaneously with zero setup friction.

Key Architectural Points:

  • # /// script Block: Standardized PEP 723 specification block declaring the Python runtime requirement (>=3.11) and required package dependencies (typer>=0.12.0).
  • from __future__ import annotations: Enables deferred evaluation of type annotations (PEP 563), allowing self-referencing classes in type hints and modern union syntax (Path | None) without runtime import overhead.
  • Core Library Imports:
    • dataclasses.dataclass: Used for high-performance, strongly typed data models (SessionData and TraceSummary).
    • datetime.datetime, timezone: Handles ISO-8601 parsing and local timezone conversion for human-friendly timestamps.
    • pathlib.Path: Cross-platform object-oriented filesystem paths for navigating ~/.gemini/**/brain/ directories.
    • typing.Annotated, Any: Powers Typer CLI option and argument declarations.
    • typer: Modern CLI library built on top of Click, using Python type annotations to generate CLI flags, help text, and argument validation.
agy-brain-explorer.py Lines 1–21
1# /// script
2# requires-python = ">=3.11"
3# dependencies = [
4# "typer>=0.12.0",
5# ]
6# ///
7
8from __future__ import annotations
9
10import contextlib
11import json
12import re
13import sys
14from dataclasses import dataclass
15from datetime import datetime, timezone
16from pathlib import Path
17from typing import Annotated, Any
18
19import typer
20
21
Data Model & Parsing Lines 22–86

2. SessionData: Core Trajectory Model & Loader

An Antigravity session directory is an organized artifact folder containing execution logs, scratch scripts, user uploads, and trajectory steps. The SessionData dataclass serves as the primary domain model representing a loaded session.

The SessionData.load() classmethod handles filesystem validation, checks for required JSON Lines (JSONL) transcripts, parses them line-by-line, and builds O(1) indexed lookups for trajectory steps.

Understanding Antigravity Transcript Types:

ℹ️
Compact (transcript.jsonl) vs. Full (transcript_full.jsonl):
Antigravity generates two transcript files under .system_generated/logs/:
  • transcript.jsonl: A token-efficient log where large fields (e.g. huge command outputs, repetitive thinking chains) are truncated, with a truncated_fields metadata array indicating truncated properties.
  • transcript_full.jsonl: The complete, untruncated transcript containing full prompts, full thinking chains, and complete tool payloads.

Method Mechanics:

  • Verification: Ensures the directory exists and contains at least one transcript file; raises a friendly typer.BadParameter error if invalid.
  • Line-by-Line Streaming: Transcripts are written in JSON Lines format (one JSON document per line). The loader parses each line with json.loads() and appends it to in-memory step lists.
  • Dual Indexing: Generates steps_by_index and full_steps_by_index dictionaries mapping integer step_index directly to the step dictionary. This enables immediate O(1) random access when drilling down into specific steps.
agy-brain-explorer.py Lines 22–86
22@dataclass
23class SessionData:
24 session_dir: Path
25 session_id: str
26 transcript_path: Path
27 transcript_full_path: Path
28 steps: list[dict[str, Any]]
29 full_steps: list[dict[str, Any]]
30 steps_by_index: dict[int, dict[str, Any]]
31 full_steps_by_index: dict[int, dict[str, Any]]
32
33 @classmethod
34 def load(cls, session_dir: Path) -> SessionData:
35 session_dir = session_dir.resolve()
36 if not session_dir.exists() or not session_dir.is_dir():
37 raise typer.BadParameter(
38 f"Directory '{session_dir}' does not exist or is not a directory."
39 )
40
41 logs_dir = session_dir / ".system_generated" / "logs"
42 transcript_path = logs_dir / "transcript.jsonl"
43 transcript_full_path = logs_dir / "transcript_full.jsonl"
44
45 if not transcript_path.exists() and not transcript_full_path.exists():
46 raise typer.BadParameter(
47 f"Directory '{session_dir}' does not appear to be an Antigravity session "
48 f"(missing {transcript_path})."
49 )
50
51 steps: list[dict[str, Any]] = []
52 if transcript_path.exists():
53 with open(transcript_path, encoding="utf-8") as f:
54 for line in f:
55 line = line.strip()
56 if line:
57 steps.append(json.loads(line))
58
59 full_steps: list[dict[str, Any]] = []
60 if transcript_full_path.exists():
61 with open(transcript_full_path, encoding="utf-8") as f:
62 for line in f:
63 line = line.strip()
64 if line:
65 full_steps.append(json.loads(line))
66 else:
67 full_steps = steps
68
69 if not steps and full_steps:
70 steps = full_steps
71
72 steps_by_index = {s["step_index"]: s for s in steps if "step_index" in s}
73 full_steps_by_index = {
74 s["step_index"]: s for s in full_steps if "step_index" in s
75 }
76
77 return cls(
78 session_dir=session_dir,
79 session_id=session_dir.name,
80 transcript_path=transcript_path,
81 transcript_full_path=transcript_full_path,
82 steps=steps,
83 full_steps=full_steps,
84 steps_by_index=steps_by_index,
85 full_steps_by_index=full_steps_by_index,
86 )
Inspection & Extraction Lines 87–160

3. Artifact Resolvers & Heuristic Prompt Parsers

During a session, Antigravity records rich contextual cues inside step payloads and directory structures. However, this metadata is often formatted in prompt templates or distributed across tool parameters.

This section provides helper methods on SessionData to resolve step output text files, extract the user's pristine request from XML tags, extract runtime configuration heuristics (such as active model selection, client local time, and mentioned workspace items), and safely format table cells for Markdown.

Inspection & Trajectory Helpers:

  • get_step_output_file(step_index): Resolves the physical path to .system_generated/steps/<step_index>/output.txt. Tool execution outputs and terminal command logs are preserved in these text files.
  • get_user_request_clean(): Antigravity encapsulates user inputs in <USER_REQUEST>...</USER_REQUEST> XML tags alongside system metadata. This method uses a regex match (with re.DOTALL) to strip prompt boilerplate and isolate the user's actual prompt text.
  • get_user_requests(): Extracts all conversational turns and prompt submissions throughout the entire trajectory, enabling multi-turn audit comparisons.
  • get_last_model_response(): Retrieves the final planner response or terminal outcome of the session to quickly assess whether the agent fulfilled the user's request.
  • get_tool_counts(): Aggregates total invocations per tool across all steps into a frequency dictionary.
  • get_start_and_end_times(): Computes starting timestamp, ending timestamp, and human-readable elapsed wallclock duration (end_time - start_time).
  • get_modified_files(): Scans tool call parameters across the session for file-writing actions (write_to_file and replace_file_content) to compile a list of all created or edited targets.
  • get_metadata_info():
    • Model Detection: Extracts the active AI model name by parsing system notification strings like The user changed setting `Model Selection` from ... to Gemini 3.8 Flash (Medium).
    • Client Local Time: Discovers the client timestamp recorded in The current local time is: ....
    • Mentions: Detects referenced files or artifacts using @[...] patterns, automatically filtering out template tokens like ITEM.
    • Workspace CWD: Scans the session's tool calls (e.g. run_command arguments) for Cwd parameters to determine which repository or workspace folder the session operated in.
  • sanitize_md_cell(): Sanitizes multiline text and unescaped pipe characters (|) so they don't break GitHub Flavored Markdown table row formatting.
agy-brain-explorer.py Lines 87–160
87
88 def get_step_output_file(self, step_index: int) -> Path | None:
89 p = (
90 self.session_dir
91 / ".system_generated"
92 / "steps"
93 / str(step_index)
94 / "output.txt"
95 )
96 return p if p.exists() else None
97
98 def get_user_request_clean(self) -> str:
99 for s in self.steps:
100 if s.get("type") == "USER_INPUT":
101 content = s.get("content", "")
102 m = re.search(
103 r"<USER_REQUEST>\s*(.*?)\s*</USER_REQUEST>", content, re.DOTALL
104 )
105 if m:
106 return m.group(1).strip()
107 return content.strip()
108 return "(none)"
109
110 def get_metadata_info(self) -> dict[str, str]:
111 meta: dict[str, str] = {}
112 for s in self.steps:
113 if s.get("type") == "USER_INPUT":
114 content = s.get("content", "")
115 # Model selection
116 m_model = re.search(
117 r"setting `Model Selection` from .*? to ([^\n\r]+?)\.\s*No need",
118 content,
119 )
120 if m_model:
121 meta["Model"] = m_model.group(1).strip()
122 # Local time
123 m_time = re.search(r"The current local time is:\s*([^\n\r.]+)", content)
124 if m_time:
125 meta["Local Time"] = m_time.group(1).strip()
126 # Mentions (exclude template keyword 'ITEM' and deduplicate)
127 all_mentions = re.findall(r"@\[([^\]]+)\]", content)
128 filtered_mentions = list(
129 dict.fromkeys(m for m in all_mentions if m != "ITEM")
130 )
131 if filtered_mentions:
132 meta["Mentioned Items"] = ", ".join(filtered_mentions)
133 break
134
135 # Workspace detection from tool calls
136 for s in self.full_steps:
137 for tc in s.get("tool_calls", []):
138 args = tc.get("args", {})
139 cwd = args.get("Cwd")
140 if cwd:
141 meta["Workspace"] = cwd
142 break
143 if "Workspace" in meta:
144 break
145 return meta
146
147
148def sanitize_md_cell(text: str | None) -> str:
149 """Sanitize text for use inside a Markdown table cell."""
150 if not text:
151 return ""
152 return (
153 text.replace("\r\n", " ")
154 .replace("\n", " ")
155 .replace("\r", " ")
156 .replace("|", "\\|")
157 .strip()
158 )
159
160
Search & Data Model Lines 161–232

4. TraceSummary & Trajectory Search Matchers

Finding past conversations requires structured metadata and robust, multi-modal query matching across heterogeneous step types (user requests, model explanations, chain-of-thought thinking, and tool invocations).

This section defines the enhanced TraceSummary record (storing both preview and untruncated prompt text) and implements two foundational search helper functions: extract_snippet() and step_matches_query().

Search Architecture & Helpers:

  • TraceSummary.full_user_request:

    While user_request contains a 90-character truncated snippet for compact table listing, full_user_request retains the complete, unshortened initial prompt. This guarantees keyword queries never miss matches that happen to occur past the 90th character of a prompt.

  • extract_snippet(text, query, max_length=120):

    Finds the query substring case-insensitively, computes a surrounding window (up to 35 characters before the match and 55 characters after), and gracefully adds leading or trailing ellipsis (...) if the text extends past the window boundaries.

  • step_matches_query(step, query):

    A multi-faceted inspector that checks if a query matches any aspect of a trajectory step:

    1. Tool Calls: Inspects tool names, CommandLine, AbsolutePath, TargetFile, toolAction, toolSummary, or any argument in the serialized arguments payload. Returns an informative tag like `run_command`: ....
    2. Content: Checks textual content for user inputs (stripping XML wrapper tags) or assistant responses.
    3. Thinking: Checks internal model chain-of-thought blocks, prefixing matched excerpts with [thinking].
agy-brain-explorer.py Lines 161–232
161@dataclass
162class TraceSummary:
163 session_id: str
164 source_brain: str
165 session_dir: Path
166 created_at: str
167 created_dt: datetime | None
168 updated_at: str
169 step_count: int
170 user_request: str
171 full_user_request: str = ""
172 model: str | None = None
173
174
175def extract_snippet(text: str, query: str, max_length: int = 120) -> str:
176 """Extract an excerpt of text surrounding the query match."""
177 if not text:
178 return ""
179 clean = text.replace("\r\n", " ").replace("\n", " ").replace("\r", " ").strip()
180 idx = clean.lower().find(query.lower())
181 if idx == -1:
182 return clean[:max_length] + ("..." if len(clean) > max_length else "")
183 start = max(0, idx - 35)
184 end = min(len(clean), idx + len(query) + 55)
185 snippet = clean[start:end].strip()
186 if start > 0:
187 snippet = "..." + snippet
188 if end < len(clean):
189 snippet = snippet + "..."
190 return snippet
191
192
193def step_matches_query(step: dict[str, Any], query: str) -> tuple[bool, str]:
194 """Check if a step matches the query and return (matched, description/snippet)."""
195 q = query.lower()
196
197 # Check tool calls
198 for tc in step.get("tool_calls", []):
199 name = tc.get("name", "")
200 args = tc.get("args", {})
201 cmd = args.get("CommandLine", "")
202 path = args.get("AbsolutePath", "") or args.get("TargetFile", "")
203 action = args.get("toolAction", "")
204 summary = args.get("toolSummary", "")
205 args_dump = json.dumps(args, ensure_ascii=False)
206 if (
207 q in name.lower()
208 or q in cmd.lower()
209 or q in path.lower()
210 or q in action.lower()
211 or q in summary.lower()
212 or q in args_dump.lower()
213 ):
214 target = cmd or path or action or summary or name
215 snippet = extract_snippet(target, query, max_length=100)
216 return True, f"`{name}`: {snippet}"
217
218 # Check content
219 content = step.get("content", "")
220 if content and q in content.lower():
221 m = re.search(r"<USER_REQUEST>\s*(.*?)\s*</USER_REQUEST>", content, re.DOTALL)
222 text = m.group(1).strip() if m else content
223 return True, extract_snippet(text, query, max_length=120)
224
225 # Check thinking
226 thinking = step.get("thinking", "")
227 if thinking and q in thinking.lower():
228 return True, f"[thinking] {extract_snippet(thinking, query, max_length=120)}"
229
230 return False, ""
231
232
Filesystem Discovery Lines 233–267

5. Multi-Brain Directory Discovery

Antigravity can be run in multiple contexts across a developer machine: through the primary GUI application, the command-line interface (agy / antigravity-cli), or specialized agent sidecars (antigravity-acp). Each context stores its conversation brains under subdirectories within ~/.gemini/.

This section implements format_datetime_display() for human-friendly timestamps and find_all_brain_directories() to automatically discover all brain storage directories across the entire system.

Multi-Brain Storage Architecture:

📂
Where Brains Live:
  • ~/.gemini/antigravity/brain/: Main Antigravity IDE sessions and paired pair-programming conversations.
  • ~/.gemini/antigravity-cli/brain/: Sessions initiated via terminal CLI commands.
  • ~/.gemini/antigravity-acp/brain/: Sessions from agentic companion protocols and headless sidecars.
  • ~/.gemini/**/brain/: Any custom or nested brain storage roots.

Discovery Strategy:

  • Targeted Globbing: First performs sorted globbing for antigravity*/brain to prioritize standard root locations.
  • Recursive Fallback: Recursively scans for any other directory named brain under ~/.gemini.
  • Deduplication via Dict Keys: Uses dict[Path, None] to preserve discovery order while ensuring no brain directory is processed twice.
  • format_datetime_display(): Converts UTC ISO-8601 timestamps into the user's current local timezone (strftime("%Y-%m-%d %H:%M")), providing immediate temporal context.
agy-brain-explorer.py Lines 233–267
233def format_datetime_display(dt: datetime | None, raw_str: str) -> str:
234 """Format datetime for Markdown display in user local time if possible."""
235 if dt is not None:
236 with contextlib.suppress(ValueError, OSError):
237 if dt.tzinfo is not None:
238 local_dt = dt.astimezone()
239 return local_dt.strftime("%Y-%m-%d %H:%M")
240 return dt.strftime("%Y-%m-%d %H:%M")
241 return raw_str.replace("T", " ")[:16]
242
243
244def find_all_brain_directories() -> list[Path]:
245 """Discover all brain directories under ~/.gemini/ (e.g. antigravity, antigravity-cli, antigravity-acp)."""
246 gemini_dir = Path.home() / ".gemini"
247 if not gemini_dir.is_dir():
248 return []
249
250 found: dict[Path, None] = {}
251 # First search for antigravity*/brain
252 for ag_dir in sorted(gemini_dir.glob("antigravity*")):
253 brain = ag_dir / "brain"
254 if brain.is_dir():
255 found[brain.resolve()] = None
256
257 # Also search for any other brain directories under ~/.gemini
258 try:
259 for brain in gemini_dir.glob("**/brain"):
260 if brain.is_dir():
261 found[brain.resolve()] = None
262 except OSError:
263 pass
264
265 return list(found.keys())
266
267
Indexing & Filtering Lines 268–392

6. Chronological Trace Discovery & Query Filtering: discover_traces()

Listing conversation histories across dozens or hundreds of sessions requires speed and accuracy. Reading entire transcript_full.jsonl files (which can exceed hundreds of megabytes) would make listing sluggish.

discover_traces() implements a high-performance scanner that inspects every session directory across all discovered brains, reads only the minimal initial lines to extract metadata and user requests, counts steps, filters by initial prompt queries when requested, and sorts all sessions chronologically.

Performance & Resilience Features:

  • Compact-First Strategy: Prefers transcript.jsonl over transcript_full.jsonl for indexing, drastically reducing I/O latency.
  • Prompt-Level Query Filtering (query parameter):

    When a search query is passed, discover_traces checks query.lower() in clean_full_req.lower() directly during iteration. Non-matching sessions are bypassed immediately before computing stat lookups or sorting, delivering instantaneous sub-second search across dozens of sessions.

  • Streaming Inspection: Iterates over the transcript line-by-line, capturing the first created_at timestamp, tracking step counts, and parsing the initial USER_INPUT step without loading full JSON objects into memory.
  • Robust Fallbacks: If a transcript lacks valid ISO timestamps or is partially written, the function gracefully falls back to filesystem metadata: directory creation time (st_ctime) and modification time (st_mtime).
  • Chronological Sorting: Sorts by creation timestamp (newest first by default, with an optional --asc flag for oldest first).
agy-brain-explorer.py Lines 268–392
268def discover_traces(
269 brain_filter: str | None = None,
270 asc: bool = False,
271 query: str | None = None,
272) -> list[TraceSummary]:
273 """Scan all brain directories under ~/.gemini and return traces sorted chronologically by creation date/time."""
274 brain_dirs = find_all_brain_directories()
275 traces: list[TraceSummary] = []
276
277 for brain in brain_dirs:
278 source_brain = brain.parent.name
279 if brain_filter and brain_filter.lower() not in source_brain.lower():
280 continue
281
282 try:
283 entries = list(brain.iterdir())
284 except OSError:
285 continue
286
287 for s in entries:
288 if not s.is_dir():
289 continue
290 logs = s / ".system_generated" / "logs"
291 t_compact = logs / "transcript.jsonl"
292 t_full = logs / "transcript_full.jsonl"
293
294 # Prefer compact for quick indexing, fallback to full
295 t_file = (
296 t_compact
297 if t_compact.exists()
298 else (t_full if t_full.exists() else None)
299 )
300 if not t_file:
301 continue
302
303 first_created_str: str | None = None
304 last_created_str: str | None = None
305 first_created_dt: datetime | None = None
306 user_req: str | None = None
307 model_name: str | None = None
308 step_count = 0
309
310 with (
311 contextlib.suppress(json.JSONDecodeError, OSError, UnicodeDecodeError),
312 open(t_file, encoding="utf-8") as f,
313 ):
314 for line in f:
315 line = line.strip()
316 if not line:
317 continue
318 step_count += 1
319 d = json.loads(line)
320
321 if "created_at" in d:
322 last_created_str = d["created_at"]
323 if first_created_str is None:
324 first_created_str = d["created_at"]
325
326 if user_req is None and d.get("type") == "USER_INPUT":
327 content = d.get("content", "")
328 m = re.search(
329 r"<USER_REQUEST>\s*(.*?)\s*</USER_REQUEST>",
330 content,
331 re.DOTALL,
332 )
333 if m:
334 user_req = m.group(1).strip()
335 else:
336 lines = content.strip().splitlines()
337 user_req = lines[0].strip() if lines else "(none)"
338 m_model = re.search(
339 r"setting `Model Selection` from .*? to ([^\n\r]+?)\.\s*No need",
340 content,
341 )
342 if m_model:
343 model_name = m_model.group(1).strip()
344
345 clean_full_req = (user_req or "(none)").replace("\n", " ").strip()
346 if query and query.lower() not in clean_full_req.lower():
347 continue
348
349 if first_created_str:
350 with contextlib.suppress(ValueError, TypeError):
351 first_created_dt = datetime.fromisoformat(
352 first_created_str.replace("Z", "+00:00")
353 )
354
355 if first_created_dt is None:
356 with contextlib.suppress(OSError, ValueError):
357 st = s.stat()
358 first_created_dt = datetime.fromtimestamp(
359 st.st_ctime, tz=timezone.utc
360 )
361 first_created_str = first_created_dt.isoformat()
362
363 if last_created_str is None:
364 with contextlib.suppress(OSError, ValueError):
365 last_created_str = datetime.fromtimestamp(
366 s.stat().st_mtime, tz=timezone.utc
367 ).isoformat()
368
369 traces.append(
370 TraceSummary(
371 session_id=s.name,
372 source_brain=source_brain,
373 session_dir=s,
374 created_at=first_created_str or "",
375 created_dt=first_created_dt,
376 updated_at=last_created_str or "",
377 step_count=step_count,
378 user_request=clean_full_req[:90],
379 full_user_request=clean_full_req,
380 model=model_name,
381 )
382 )
383
384 def sort_key(t: TraceSummary) -> float:
385 if t.created_dt:
386 return t.created_dt.timestamp()
387 return 0.0
388
389 traces.sort(key=sort_key, reverse=not asc)
390 return traces
391
392
Fuzzy Resolution Lines 393–473

7. Flexible Session Resolution: find_session_directory()

Developers and AI agents rarely want to type or memorize a 36-character UUID like 5c074549-b840-4999-8aaf-38657370af9e. Often, they want to provide a short 8-character prefix (5c074549), a relative path (./my_session), or an absolute folder path.

find_session_directory() provides multi-modal resolution, searching direct paths, local workspace folders, and all global brain repositories with unambiguous prefix matching.

Resolution Cascade:

  1. Direct Filesystem Path: Checks if the argument is an explicit directory (relative or absolute, expanding ~), verifying it contains a transcript.
  2. Current Working Directory: Checks Path.cwd() / session_arg for local session folders.
  3. Exact Brain Matches: Searches all known ~/.gemini/**/brain/ directories for an exact folder name match. If multiple brains contain the same ID, it resolves to the most recently modified.
  4. Prefix / Partial Matches: Matches the leading characters of session folder names (case-insensitive):
    • If exactly one session matches, it resolves cleanly.
    • If multiple sessions match the prefix, it raises a descriptive typer.BadParameter error displaying the ambiguous candidates.
    • If none match, it provides a comprehensive error listing all brain folders searched.
agy-brain-explorer.py Lines 393–473
393def find_session_directory(session_arg: str) -> Path:
394 """Resolve session_arg to a directory by checking:
395
396 1. Explicit path (relative or absolute, expanded)
397 2. Local directory in CWD
398 3. Exact and prefix matches in all ~/.gemini/**/brain/ directories
399 """
400 raw_path = Path(session_arg).expanduser()
401 try:
402 resolved_path = raw_path.resolve()
403 except OSError:
404 resolved_path = raw_path
405
406 # Check direct path first
407 if resolved_path.is_dir():
408 logs_dir = resolved_path / ".system_generated" / "logs"
409 if (logs_dir / "transcript.jsonl").exists() or (
410 logs_dir / "transcript_full.jsonl"
411 ).exists():
412 return resolved_path
413
414 # Check relative to CWD
415 local_path = (Path.cwd() / session_arg).resolve()
416 if local_path.is_dir():
417 logs_dir = local_path / ".system_generated" / "logs"
418 if (logs_dir / "transcript.jsonl").exists() or (
419 logs_dir / "transcript_full.jsonl"
420 ).exists():
421 return local_path
422
423 # Search across all brain directories in ~/.gemini
424 brain_dirs = find_all_brain_directories()
425
426 # 1. Exact match in brain directories
427 exact_matches: list[Path] = []
428 for brain in brain_dirs:
429 target = brain / session_arg
430 if target.is_dir():
431 exact_matches.append(target)
432
433 if len(exact_matches) == 1:
434 return exact_matches[0]
435 elif len(exact_matches) > 1:
436 return max(exact_matches, key=lambda p: p.stat().st_mtime)
437
438 # 2. Prefix / partial match in brain directories
439 clean_arg = session_arg.lower().strip()
440 partial_matches: list[Path] = []
441 for brain in brain_dirs:
442 try:
443 for entry in brain.iterdir():
444 if entry.is_dir() and entry.name.lower().startswith(clean_arg):
445 partial_matches.append(entry)
446 except PermissionError:
447 continue
448
449 if len(partial_matches) == 1:
450 return partial_matches[0]
451 elif len(partial_matches) > 1:
452 match_list = "\n - ".join(
453 f"{p.name} ({p.parent.parent.name})" for p in partial_matches[:10]
454 )
455 raise typer.BadParameter(
456 f"Multiple sessions matched '{session_arg}':\n - {match_list}\n"
457 "Please specify the full session ID or an explicit path."
458 )
459
460 # If resolved_path exists as a dir, return it so SessionData.load can give a specific missing transcript error
461 if resolved_path.is_dir():
462 return resolved_path
463
464 searched_dirs = (
465 "\n - ".join(str(b) for b in brain_dirs) if brain_dirs else "(none found)"
466 )
467 raise typer.BadParameter(
468 f"Session '{session_arg}' could not be found as a directory path or inside Antigravity brain folders.\n"
469 f"Checked path: {resolved_path}\n"
470 f"Searched brain folders:\n - {searched_dirs}"
471 )
472
473
Presentation & REPL Lines 474–614

8. Trace Explorer Presentation & Interactive REPL

When an agent or user runs the explorer, results should be clear, visually structured, and immediately actionable. This module contains run_interactive_session_picker() (an interactive terminal browser) and run_explorer() (which formats discovered traces into Markdown tables with clickable links and query badges).

Interactive & Markdown Capabilities:

  • Clickable Antigravity Links:

    Session IDs are formatted as [`<short_id>`](conversation://<full_session_uuid>). In Antigravity's chat interface, this renders as a native interactive link that directly opens the past conversation in the workspace UI!

  • Query-Aware Table Presentation:

    When a query filter is active (via --query), the table header highlights the active search filter, and the prompt column dynamically renders a localized snippet with context around the matched term rather than a static 90-character truncate.

  • Interactive REPL (run_interactive_session_picker):

    Triggered with --interactive or -i. Users can enter a numerical index (e.g. 1) or partial UUID to select a session, and then jump directly into its summary, timeline, tool calls, or commands in an interactive menu loop without re-running shell commands.

agy-brain-explorer.py Lines 474–614
474# ---------------------------------------------------------------------------
475# Explorer Presentation & CLI
476# ---------------------------------------------------------------------------
477
478
479def run_interactive_session_picker(traces: list[TraceSummary]) -> None:
480 """Allow interactive selection and exploration of traces."""
481 print("\nInteractive Trace Explorer")
482 while True:
483 try:
484 choice = input(
485 f"\nEnter trace #[1-{len(traces)}] or session ID to inspect (q to quit): "
486 ).strip()
487 except (KeyboardInterrupt, EOFError):
488 break
489
490 if choice.lower() in ("q", "quit", "exit"):
491 break
492 if not choice:
493 continue
494
495 selected_trace: TraceSummary | None = None
496 if choice.isdigit():
497 idx = int(choice)
498 if 1 <= idx <= len(traces):
499 selected_trace = traces[idx - 1]
500 else:
501 print(
502 f"Error: Index {idx} out of range (1-{len(traces)}).",
503 file=sys.stderr,
504 )
505 continue
506 else:
507 matches = [
508 t for t in traces if t.session_id.lower().startswith(choice.lower())
509 ]
510 if len(matches) == 1:
511 selected_trace = matches[0]
512 elif len(matches) > 1:
513 print(
514 f"Error: Multiple traces match '{choice}'. Please provide more characters.",
515 file=sys.stderr,
516 )
517 continue
518 else:
519 print(
520 f"Error: No trace matches '{choice}'.",
521 file=sys.stderr,
522 )
523 continue
524
525 print(
526 f"\nSelected session: {selected_trace.session_id} ({selected_trace.source_brain})"
527 )
528 session_data = SessionData.load(selected_trace.session_dir)
529
530 action = (
531 input(
532 "Action: [1] Summary (default), [2] Steps, [3] Tools, [4] Commands, [b] Back: "
533 )
534 .strip()
535 .lower()
536 )
537
538 if action in ("b", "back"):
539 continue
540 elif action in ("2", "steps", "s"):
541 show_steps(session_data, limit=30)
542 elif action in ("3", "tools", "t"):
543 show_tools(session_data)
544 elif action in ("4", "commands", "c"):
545 show_commands(session_data)
546 else:
547 show_summary(session_data)
548
549
550def run_explorer(
551 limit: int | None = 30,
552 asc: bool = False,
553 brain_filter: str | None = None,
554 query: str | None = None,
555 interactive: bool = False,
556) -> None:
557 """List all traces in Markdown ordered chronologically by creation date/time."""
558 traces = discover_traces(brain_filter=brain_filter, asc=asc, query=query)
559
560 if not traces:
561 print("*No Antigravity conversation traces found under ~/.gemini.*")
562 filters = []
563 if brain_filter:
564 filters.append(f"`--brain {brain_filter}`")
565 if query:
566 filters.append(f"`--query {query}`")
567 if filters:
568 print(f"\n*Filters applied: {', '.join(filters)}*")
569 return
570
571 traces_to_show = traces[:limit] if limit else traces
572 order_str = "oldest first" if asc else "newest first"
573 header = f"### Antigravity Conversation Traces ({len(traces)} total"
574 if brain_filter:
575 header += f", filter: `{brain_filter}`"
576 if query:
577 header += f", query: `{query}`"
578 header += f", {order_str})\n"
579 print(header)
580 print("| # | Created | Brain | Session ID | Steps | User Request |")
581 print("| :--- | :--- | :--- | :--- | :---: | :--- |")
582 for idx, t in enumerate(traces_to_show, 1):
583 created_display = format_datetime_display(t.created_dt, t.created_at)
584 if query:
585 req_display = extract_snippet(
586 t.full_user_request or t.user_request, query, max_length=120
587 )
588 else:
589 req_display = t.user_request[:100]
590 req_sanitized = sanitize_md_cell(req_display)
591 session_link = f"[`{t.session_id[:8]}`](conversation://{t.session_id})"
592 print(
593 f"| {idx} | {created_display} | {t.source_brain} | {session_link} | {t.step_count} | {req_sanitized} |"
594 )
595 if limit and len(traces) > limit:
596 print(
597 f"\n*Showing {len(traces_to_show)} of {len(traces)} traces. Use `--all` or `--limit` to show more.*"
598 )
599
600 if interactive:
601 run_interactive_session_picker(traces_to_show)
602
603
604explorer_app = typer.Typer(
605 help=(
606 "Antigravity Session & Trace Exploration CLI: explore all conversation traces across "
607 "~/.gemini brain directories, or inspect specific session transcripts, tool calls, and execution steps."
608 ),
609 no_args_is_help=False,
610 add_completion=False,
611 context_settings={"help_option_names": ["-h", "--help"]},
612)
613
614
CLI Interface Lines 615–688

9. Trace Explorer Typer Application

Typer turns standard Python function signatures and type annotations into rich command-line interfaces. This section defines the explorer_app instance and its root callback, which handles discovery-mode CLI flags and prompt query filtering.

CLI Options & Defaults:

  • --query, -q, --search, -s (string):

    Instant prompt keyword filter. Narrows discovered traces to sessions where the initial request contains the query string (e.g. -q "build a unified static HTML").

  • --limit, -n (default: 30): Caps the number of conversation traces displayed to avoid terminal buffer overflow.
  • --all, -a (flag): Bypasses pagination to list every discovered session across all brains.
  • --asc (flag): Inverts chronological sorting to display oldest traces first.
  • --brain, -b (string): Filters discovery to a specific brain subfolder (e.g. antigravity or antigravity-cli).
  • --interactive, -i (flag): Launches the interactive trace explorer loop.
  • invoke_without_command=True: Allows the explorer application to run immediately when no subcommands are supplied.
agy-brain-explorer.py Lines 615–688
615@explorer_app.callback(invoke_without_command=True)
616def explorer_callback(
617 ctx: typer.Context,
618 limit: Annotated[
619 int | None,
620 typer.Option(
621 "--limit",
622 "-n",
623 help="Limit number of traces displayed (default: 30).",
624 ),
625 ] = 30,
626 all_traces: Annotated[
627 bool,
628 typer.Option(
629 "--all",
630 "-a",
631 help="Show all discovered traces without pagination limit.",
632 ),
633 ] = False,
634 asc: Annotated[
635 bool,
636 typer.Option(
637 "--asc",
638 help="Order traces oldest first (default: newest first).",
639 ),
640 ] = False,
641 brain: Annotated[
642 str | None,
643 typer.Option(
644 "--brain",
645 "-b",
646 help="Filter traces by brain root directory name (e.g. 'antigravity-cli').",
647 ),
648 ] = None,
649 query: Annotated[
650 str | None,
651 typer.Option(
652 "--query",
653 "-q",
654 "--search",
655 "-s",
656 help="Filter traces by text occurring in the initial user request.",
657 ),
658 ] = None,
659 as_markdown: Annotated[
660 bool,
661 typer.Option(
662 "--markdown",
663 "--md",
664 "-m",
665 help="Output trace listing as Markdown formatted content.",
666 ),
667 ] = True,
668 interactive: Annotated[
669 bool,
670 typer.Option(
671 "--interactive",
672 "-i",
673 help="Interactively select a session from the list to inspect.",
674 ),
675 ] = False,
676) -> None:
677 if ctx.invoked_subcommand is not None:
678 return
679 effective_limit = None if all_traces else limit
680 run_explorer(
681 limit=effective_limit,
682 asc=asc,
683 brain_filter=brain,
684 query=query,
685 interactive=interactive,
686 )
687
688
Cross-Session Search Lines 689–836

10. Global Cross-Session & Deep Search: cmd_explorer_search

Locating past work is a critical developer requirement. Sometimes you remember what you asked in the initial prompt; other times you only remember a specific file created, command executed, or tool error encountered deep within a 200-step trajectory.

cmd_explorer_search() implements a dual-mode search command on explorer_app, supporting fast prompt matching and comprehensive deep-transcript inspection across all sessions.

Dual-Mode Search Capabilities:

  • Prompt-Only Search (Default, all_steps=False):

    Invoked via agy-brain-explorer.py search <QUERY>. Leverages the fast discover_traces(query=...) engine to return sessions matching the initial request, showing clickable conversation links, brain origins, step counts, and formatted prompt excerpts.

  • Deep Step Search (--all-steps / -s):

    Invoked via agy-brain-explorer.py search <QUERY> --all-steps. Iterates through every discovered session directory, streaming its transcript steps and evaluating each step against step_matches_query().

    Renders a unified timeline match table detailing:

    • Clickable session identifier ([`<short_id>`](conversation://<uuid>))
    • Originating brain environment
    • Exact step index where the match occurred
    • Step type (e.g. PLANNER_RESPONSE, GENERIC, USER_INPUT)
    • Extracted snippet showing the matching tool call, argument, or content
  • Cross-Session Comparative & Path Commands:

    In addition to search, explorer_app exposes:

    • cmd_explorer_compare(session1, session2): Compares two sessions side-by-side across timing, user turns, tool frequencies, modified files, and outcomes.
    • cmd_explorer_paths(sessions): Resolves and outputs on-disk filesystem targets (directories, JSONL transcripts, logs, step folders) for one or multiple sessions simultaneously.
agy-brain-explorer.py Lines 689–836
689@explorer_app.command(name="search")
690def cmd_explorer_search(
691 query: Annotated[
692 str,
693 typer.Argument(help="Search query text or keyword."),
694 ],
695 all_steps: Annotated[
696 bool,
697 typer.Option(
698 "--all-steps",
699 "-s",
700 help="Deep search across all conversation steps (user requests, model responses, tool invocations, commands). Default searches initial user request only.",
701 ),
702 ] = False,
703 brain: Annotated[
704 str | None,
705 typer.Option(
706 "--brain",
707 "-b",
708 help="Filter search to specific brain root directory (e.g. 'antigravity-cli').",
709 ),
710 ] = None,
711 limit: Annotated[
712 int | None,
713 typer.Option(
714 "--limit",
715 "-n",
716 help="Limit number of results returned (default: 30).",
717 ),
718 ] = 30,
719 all_results: Annotated[
720 bool,
721 typer.Option(
722 "--all",
723 "-a",
724 help="Return all results without pagination limit.",
725 ),
726 ] = False,
727) -> None:
728 """Search conversation sessions by initial prompt, or deeply across all steps."""
729 effective_limit = None if all_results else limit
730
731 if not all_steps:
732 traces = discover_traces(brain_filter=brain, query=query)
733 header = f"### Session Search Results for \"{query}\" in Initial User Requests ({len(traces)} sessions found"
734 if brain:
735 header += f", brain: `{brain}`"
736 header += ")\n"
737 print(header)
738
739 if not traces:
740 print("*No sessions matched your query in their initial user prompt.*")
741 return
742
743 traces_to_show = traces[:effective_limit] if effective_limit else traces
744 print("| # | Created | Brain | Session ID | Steps | Matched User Request |")
745 print("| :--- | :--- | :--- | :--- | :---: | :--- |")
746 for idx, t in enumerate(traces_to_show, 1):
747 created_display = format_datetime_display(t.created_dt, t.created_at)
748 req_text = t.full_user_request or t.user_request
749 snippet = extract_snippet(req_text, query, max_length=120)
750 session_link = f"[`{t.session_id[:8]}`](conversation://{t.session_id})"
751 print(
752 f"| {idx} | {created_display} | {t.source_brain} | {session_link} | {t.step_count} | {sanitize_md_cell(snippet)} |"
753 )
754
755 if effective_limit and len(traces) > effective_limit:
756 print(
757 f"\n*Showing {len(traces_to_show)} of {len(traces)} sessions. Use `--all` or `--limit` to show more.*"
758 )
759 else:
760 brain_dirs = find_all_brain_directories()
761 step_matches: list[dict[str, Any]] = []
762
763 for b in brain_dirs:
764 source_brain = b.parent.name
765 if brain and brain.lower() not in source_brain.lower():
766 continue
767 try:
768 entries = list(b.iterdir())
769 except OSError:
770 continue
771
772 for s_dir in entries:
773 if not s_dir.is_dir():
774 continue
775 logs = s_dir / ".system_generated" / "logs"
776 t_compact = logs / "transcript.jsonl"
777 t_full = logs / "transcript_full.jsonl"
778 t_file = (
779 t_compact
780 if t_compact.exists()
781 else (t_full if t_full.exists() else None)
782 )
783 if not t_file:
784 continue
785
786 with (
787 contextlib.suppress(
788 json.JSONDecodeError, OSError, UnicodeDecodeError
789 ),
790 open(t_file, encoding="utf-8") as f,
791 ):
792 for line in f:
793 line = line.strip()
794 if not line:
795 continue
796 step_data = json.loads(line)
797 matched, desc = step_matches_query(step_data, query)
798 if matched:
799 step_matches.append(
800 {
801 "session_id": s_dir.name,
802 "source_brain": source_brain,
803 "step_index": step_data.get("step_index", 0),
804 "type": step_data.get("type", ""),
805 "created_at": step_data.get("created_at", ""),
806 "desc": desc,
807 }
808 )
809
810 header = f"### Deep Search Results for \"{query}\" across All Steps ({len(step_matches)} steps matched"
811 if brain:
812 header += f", brain: `{brain}`"
813 header += ")\n"
814 print(header)
815
816 if not step_matches:
817 print("*No steps matched your query across discovered sessions.*")
818 return
819
820 to_show = step_matches[:effective_limit] if effective_limit else step_matches
821 print("| # | Session ID | Brain | Step | Type | Matched Excerpt |")
822 print("| :--- | :--- | :--- | :---: | :--- | :--- |")
823 for idx, m in enumerate(to_show, 1):
824 session_link = f"[`{m['session_id'][:8]}`](conversation://{m['session_id']})"
825 clean_desc = sanitize_md_cell(m["desc"])
826 print(
827 f"| {idx} | {session_link} | {m['source_brain']} | {m['step_index']} | {m['type']} | {clean_desc} |"
828 )
829
830 if effective_limit and len(step_matches) > effective_limit:
831 print(
832 f"\n*Showing {len(to_show)} of {len(step_matches)} matches. Use `--all` or `--limit` to show more.*"
833 )
834
835
836
Session Overview Lines 837–911

11. Single-Session Inspection & Summary Renderer

When focusing on a specific session, developers need a comprehensive high-level summary: when it started, when it finished, how long it took, what model was driving it, which workspace it modified, and what tools were utilized.

show_summary() computes these metrics and formats them into a clean Markdown overview document.

Computed Metrics & Renderers:

  • show_summary(data): Computes session metrics and renders a Markdown overview table:
    • Clickable URI Links: Generates file:// links for the session directory, compact transcript.jsonl, full transcript_full.jsonl, and active workspace CWD.
    • Session Timing & Duration: Calculates exact elapsed time between initial prompt timestamp and final response step (end_time - start_time).
    • Artifact Counting: Inspects physical disk directories (scratch/ and .user_uploaded/).
    • Tool Usage Frequency Table: Aggregates all tool_calls across all steps in the session, sorted in descending frequency.
  • show_paths(data) & show_paths_multi(sessions_data): Formats detailed on-disk component locations (session directory, transcript files with sizes and step counts, logs directory, step output folders, scratch directory, and workspace) with clickable file:// links.
  • show_comparison(s1, s2): Renders a comprehensive side-by-side Markdown comparison matrix between two sessions:
    • Compares metadata, timing, duration, step counts, user turns, total tool calls, files written, and model configuration.
    • Identifies identical vs diverged initial user prompts and outlines subsequent follow-up user turns.
    • Produces an itemized side-by-side tool call count table across all invoked tools.
    • Lists all created or modified files with clickable file:// links.
    • Previews the closing assistant responses and outcomes for both trajectories.
agy-brain-explorer.py Lines 837–911
837# ---------------------------------------------------------------------------
838# Single Session Inspection CLI
839# ---------------------------------------------------------------------------
840
841session_app = typer.Typer(
842 help="Inspect a specific Antigravity session transcript, steps, and tool execution.",
843 no_args_is_help=True,
844 add_completion=False,
845 context_settings={"help_option_names": ["-h", "--help"]},
846)
847
848
849def show_summary(data: SessionData) -> None:
850 """Render session summary as Markdown."""
851 start_time: datetime | None = None
852 end_time: datetime | None = None
853 if data.steps:
854 try:
855 start_time = datetime.fromisoformat(data.steps[0].get("created_at", ""))
856 end_time = datetime.fromisoformat(data.steps[-1].get("created_at", ""))
857 except (ValueError, TypeError):
858 pass
859
860 duration_str = (
861 str(end_time - start_time) if (start_time and end_time) else "unknown"
862 )
863
864 meta = data.get_metadata_info()
865 scratch_items = (
866 list((data.session_dir / "scratch").glob("*"))
867 if (data.session_dir / "scratch").exists()
868 else []
869 )
870 uploaded_items = (
871 list((data.session_dir / ".user_uploaded").glob("*"))
872 if (data.session_dir / ".user_uploaded").exists()
873 else []
874 )
875 user_req = data.get_user_request_clean()
876
877 tool_counts: dict[str, int] = {}
878 for s in data.full_steps:
879 for tc in s.get("tool_calls", []):
880 name = tc.get("name", "unknown")
881 tool_counts[name] = tool_counts.get(name, 0) + 1
882
883 print(f"### Session Overview: `{data.session_id}`\n")
884 print("| Property | Value |")
885 print("| :--- | :--- |")
886 print(
887 f"| **Session ID** | [`{data.session_id}`](conversation://{data.session_id}) |"
888 )
889 print(f"| **Directory** | `{data.session_dir}` |")
890 print(f"| **Started At** | {start_time if start_time else 'unknown'} |")
891 print(f"| **Ended At** | {end_time if end_time else 'unknown'} |")
892 print(f"| **Duration** | {duration_str} |")
893 print(f"| **Total Steps** | {len(data.steps)} |")
894 for k, v in meta.items():
895 print(f"| **{sanitize_md_cell(k)}** | {sanitize_md_cell(str(v))} |")
896 print(f"| **Scratch Files** | {len(scratch_items)} |")
897 print(f"| **Uploaded Files** | {len(uploaded_items)} |\n")
898
899 print("#### Initial User Request")
900 print(f"> {sanitize_md_cell(user_req)}\n")
901
902 if tool_counts:
903 print("#### Tool Invocations")
904 print("| Tool Name | Count |")
905 print("| :--- | :---: |")
906 for tool, count in sorted(
907 tool_counts.items(), key=lambda item: item[1], reverse=True
908 ):
909 print(f"| `{tool}` | {count} |")
910
911
Provenance & Timeline Lines 912–1017

12. Execution Lineage & Timeline Browser

In Antigravity agent trajectories, an action dispatched by the model (e.g., executing a bash command or writing a file) produces a separate result step of type GENERIC. In raw logs, these result steps lack an explicit forward pointer to why they happened.

This module implements backward provenance resolution via find_triggering_step(), linking results back to their parent tool calls, and powers the chronological timeline browser show_steps().

Backward Lineage Resolution (find_triggering_step):

🔗
How Provenance Tracking Works:
When encountering a GENERIC step at index K, the algorithm walks backward through steps (K-1, K-2, ...) until it finds the preceding step containing tool_calls. If it hits an unrelated prompt or response boundary first, it halts. This establishes the causal link: "Step 8 is the tool output of Step 7's run_command call."

Timeline Filtering in show_steps():

  • --tools-only: Filters timeline to steps where tools were invoked.
  • --user-only: Filters timeline to user prompts.
  • Offset & Limit: Supports slicing and pagination across long agent conversations.
  • Smart Previews: Automatically formats action summaries, user requests, or tool names into a single clean line suitable for table rendering.
agy-brain-explorer.py Lines 912–1017
912def find_triggering_step(
913 data: SessionData, step_index: int
914) -> tuple[int, dict[str, Any]] | None:
915 """Find the preceding step that dispatched tool calls resulting in step_index."""
916 s = data.full_steps_by_index.get(step_index)
917 if not s or s.get("type") != "GENERIC":
918 return None
919 for idx in range(step_index - 1, -1, -1):
920 candidate = data.full_steps_by_index.get(idx)
921 if candidate and candidate.get("tool_calls"):
922 return idx, candidate
923 if candidate and candidate.get("type") in ("USER_INPUT", "PLANNER_RESPONSE"):
924 break
925 return None
926
927
928def show_steps(
929 data: SessionData,
930 tools_only: bool = False,
931 user_only: bool = False,
932 limit: int | None = None,
933 offset: int = 0,
934) -> None:
935 """Render conversation timeline steps as Markdown."""
936 matched = 0
937 md_results: list[tuple[int, str, str, str, str]] = []
938
939 for s in data.full_steps:
940 idx = s.get("step_index", 0)
941 source = s.get("source", "")
942 tp = s.get("type", "")
943 tool_calls = s.get("tool_calls", [])
944
945 if tools_only and not tool_calls:
946 continue
947 if user_only and tp != "USER_INPUT":
948 continue
949
950 if matched < offset:
951 matched += 1
952 continue
953
954 if limit is not None and matched >= offset + limit:
955 break
956
957 time_str = ""
958 try:
959 time_str = datetime.fromisoformat(s.get("created_at", "")).strftime(
960 "%H:%M:%S"
961 )
962 except (ValueError, TypeError):
963 pass
964
965 # Build preview string
966 if tool_calls:
967 calls_desc = []
968 for tc in tool_calls:
969 name = tc.get("name", "tool")
970 args = tc.get("args", {})
971 action = (
972 args.get("toolAction")
973 or args.get("CommandLine")
974 or args.get("AbsolutePath")
975 or ""
976 )
977 if action:
978 calls_desc.append(f"{name} ({action})")
979 else:
980 calls_desc.append(name)
981 clean_preview = "; ".join(calls_desc)
982 elif tp == "USER_INPUT":
983 content = s.get("content", "")
984 m = re.search(
985 r"<USER_REQUEST>\s*(.*?)\s*</USER_REQUEST>", content, re.DOTALL
986 )
987 clean_text = m.group(1).strip() if m else content.strip()
988 clean_preview = clean_text.replace("\n", " ")[:90]
989 elif tp == "GENERIC":
990 trigger = find_triggering_step(data, idx)
991 if trigger:
992 parent_idx, parent_step = trigger
993 parent_tools = parent_step.get("tool_calls", [])
994 tools_desc = ", ".join(tc.get("name", "tool") for tc in parent_tools)
995 clean_preview = f"Result of Step {parent_idx} ({tools_desc})"
996 else:
997 content = s.get("content", "")
998 first_line = (
999 content.strip().splitlines()[0] if content.strip() else "(empty)"
1000 )
1001 clean_preview = first_line[:90]
1002 else:
1003 content = s.get("content", "")
1004 clean_preview = content.replace("\n", " ")[:90]
1005
1006 md_results.append(
1007 (idx, time_str, source, tp, sanitize_md_cell(clean_preview))
1008 )
1009 matched += 1
1010
1011 print(f"### Timeline for `{data.session_id}`\n")
1012 print("| Step | Time | Source | Type | Preview / Action |")
1013 print("| :---: | :--- | :--- | :--- | :--- |")
1014 for s_idx, t_str, src, s_type, p_view in md_results:
1015 print(f"| {s_idx} | {t_str} | `{src}` | `{s_type}` | {p_view} |")
1016
1017
Security & Auditing Lines 1018–1063

13. Audit Views: Tool Invocations & Shell Commands

Security and execution integrity are paramount when working with autonomous AI agents. Developers must be able to audit exactly what tools the agent executed, what files were inspected or altered, and what shell commands were run.

This section provides show_tools() and show_commands(), two specialized audit renderers designed for rapid inspection.

Audit Focus Areas:

  • show_tools():

    Iterates through every tool invocation in the trajectory. It extracts the tool name, human-readable action summary (from toolAction or toolSummary), and target arguments (such as CommandLine, AbsolutePath, TargetFile, or DirectoryPath).

  • show_commands():

    Exclusively isolates terminal shell executions (run_command). Displays a table pairing each command's step index with its exact working directory (Cwd) and the full shell command string executed. This makes it effortless to verify what commands were run during a build, test, or deployment task.

agy-brain-explorer.py Lines 1018–1063
1018def show_tools(data: SessionData) -> None:
1019 """Render list of all tool invocations as Markdown."""
1020 print(f"### Tool Invocations for `{data.session_id}`\n")
1021 print("| Step | Tool | Action / Summary | Parameters / Target |")
1022 print("| :---: | :--- | :--- | :--- |")
1023 for s in data.full_steps:
1024 idx = s.get("step_index", 0)
1025 for tc in s.get("tool_calls", []):
1026 name = tc.get("name", "")
1027 args = tc.get("args", {})
1028 action = args.get("toolAction") or args.get("toolSummary") or ""
1029 target = ""
1030 if "CommandLine" in args:
1031 target = f"`{args['CommandLine']}` (cwd: `{args.get('Cwd', '')}`)"
1032 elif "AbsolutePath" in args:
1033 target = f"`{args['AbsolutePath']}`"
1034 elif "DirectoryPath" in args:
1035 target = f"`{args['DirectoryPath']}`"
1036 elif "TargetFile" in args:
1037 target = f"`{args['TargetFile']}`"
1038 else:
1039 target = json.dumps(
1040 {k: v for k, v in args.items() if not k.startswith("tool")}
1041 )[:80]
1042 print(
1043 f"| {idx} | `{name}` | {sanitize_md_cell(action)} | {sanitize_md_cell(target)} |"
1044 )
1045
1046
1047def show_commands(data: SessionData) -> None:
1048 """Render list of all executed shell commands as Markdown."""
1049 print(f"### Executed Shell Commands for `{data.session_id}`\n")
1050 print("| Step | Working Directory (Cwd) | Command Line |")
1051 print("| :---: | :--- | :--- |")
1052 for s in data.full_steps:
1053 idx = s.get("step_index", 0)
1054 for tc in s.get("tool_calls", []):
1055 if tc.get("name") == "run_command":
1056 args = tc.get("args", {})
1057 cmd = args.get("CommandLine", "")
1058 cwd = args.get("Cwd", "")
1059 print(
1060 f"| {idx} | `{sanitize_md_cell(cwd)}` | `{sanitize_md_cell(cmd)}` |"
1061 )
1062
1063
Subcommand Routing Lines 1064–1164

14. Session CLI Subcommands: summary & steps

The session_app Typer application handles commands scoped to an individual session. It uses a root callback to resolve the session identifier into loaded SessionData and injects it into Typer's context object (ctx.obj).

This section defines session_callback, cmd_summary, and cmd_steps.

Context Injection & Routing:

  • session_callback:

    Takes the positional SESSION argument (UUID, prefix, or directory path), calls find_session_directory(), and loads SessionData into ctx.obj so all downstream subcommands can access it without reloading transcripts.

    If no subcommand is supplied (e.g. agy-brain-explorer.py 4c6b51ee), it automatically delegates to show_summary().

  • cmd_summary: Explicitly triggers the session overview via agy-brain-explorer.py <SESSION> summary.
  • cmd_steps: Implements the timeline browser with CLI flags:
    • --tools-only / -t
    • --user-only / -u
    • --limit / -n
    • --offset
agy-brain-explorer.py Lines 1064–1164
1064@session_app.callback(invoke_without_command=True)
1065def session_callback(
1066 ctx: typer.Context,
1067 session: Annotated[
1068 str,
1069 typer.Argument(
1070 metavar="SESSION",
1071 help="Path to session directory OR session ID (searched in ~/.gemini/**/brain).",
1072 ),
1073 ],
1074 as_markdown: Annotated[
1075 bool,
1076 typer.Option(
1077 "--markdown",
1078 "--md",
1079 "-m",
1080 help="Output results as Markdown formatted content.",
1081 ),
1082 ] = True,
1083) -> None:
1084 """Initialize session data from a path or session ID (defaults to summary if no subcommand)."""
1085 session_dir = find_session_directory(session)
1086 ctx.obj = SessionData.load(session_dir)
1087
1088 if ctx.invoked_subcommand is None:
1089 show_summary(ctx.obj)
1090
1091
1092@session_app.command(name="summary")
1093def cmd_summary(
1094 ctx: typer.Context,
1095 as_markdown: Annotated[
1096 bool,
1097 typer.Option(
1098 "--markdown",
1099 "--md",
1100 "-m",
1101 help="Output results as Markdown formatted content.",
1102 ),
1103 ] = True,
1104) -> None:
1105 """Show high-level overview of the session, metadata, timing, and tool statistics as Markdown."""
1106 data: SessionData = ctx.obj
1107 show_summary(data)
1108
1109
1110@session_app.command(name="steps")
1111def cmd_steps(
1112 ctx: typer.Context,
1113 tools_only: Annotated[
1114 bool,
1115 typer.Option(
1116 "--tools-only",
1117 "-t",
1118 help="Only show steps with tool calls.",
1119 ),
1120 ] = False,
1121 user_only: Annotated[
1122 bool,
1123 typer.Option(
1124 "--user-only",
1125 "-u",
1126 help="Only show user input steps.",
1127 ),
1128 ] = False,
1129 limit: Annotated[
1130 int | None,
1131 typer.Option(
1132 "--limit",
1133 "-n",
1134 help="Limit number of steps shown.",
1135 ),
1136 ] = None,
1137 offset: Annotated[
1138 int,
1139 typer.Option(
1140 "--offset",
1141 help="Number of steps to skip.",
1142 ),
1143 ] = 0,
1144 as_markdown: Annotated[
1145 bool,
1146 typer.Option(
1147 "--markdown",
1148 "--md",
1149 "-m",
1150 help="Output results as Markdown formatted content.",
1151 ),
1152 ] = True,
1153) -> None:
1154 """List timeline of conversation steps as Markdown."""
1155 data: SessionData = ctx.obj
1156 show_steps(
1157 data,
1158 tools_only=tools_only,
1159 user_only=user_only,
1160 limit=limit,
1161 offset=offset,
1162 )
1163
1164
Forensics & Debugging Lines 1165–1295

15. Deep Step Inspection: cmd_step

When debugging an agent failure, examining a tool error, or reviewing model reasoning, developers need to inspect every facet of a single step. cmd_step() is the diagnostic workhorse of the explorer.

It reconstructs the entire step state: metadata, causal parent tool calls, textual content, model thinking / chain-of-thought, pretty-printed JSON tool arguments, and external execution log files.

Step Forensics Components:

  • Triggering Action Provenance: If the inspected step is a tool result, the command displays the parent tool name, action summary, and exact invocation parameters (e.g. command line and directory).
  • Model Thinking (Chain-of-Thought): If the step contains model reasoning (thinking field), it prints the model's internal rationale inside a code block.
  • Pretty-Printed Tool Calls: Formats all tool call argument dictionaries using json.dumps(args, indent=2).
  • External Step Output (output.txt): Reads the associated physical log file from .system_generated/steps/<step_index>/output.txt. Includes a duplicate check (preventing redundant printing if the output is already identical to content) and supports truncation via --lines / -l (default: 50 lines).
agy-brain-explorer.py Lines 1165–1295
1165@session_app.command(name="step")
1166def cmd_step(
1167 ctx: typer.Context,
1168 step_index: Annotated[
1169 int,
1170 typer.Argument(help="Step index to inspect."),
1171 ],
1172 max_lines: Annotated[
1173 int,
1174 typer.Option(
1175 "--lines",
1176 "-l",
1177 help="Max lines of output to display.",
1178 ),
1179 ] = 50,
1180 as_markdown: Annotated[
1181 bool,
1182 typer.Option(
1183 "--markdown",
1184 "--md",
1185 "-m",
1186 help="Output results as Markdown formatted content.",
1187 ),
1188 ] = True,
1189) -> None:
1190 """Inspect detailed information, tool calls, and output of a specific step in Markdown."""
1191 data: SessionData = ctx.obj
1192
1193 s = data.full_steps_by_index.get(step_index)
1194 if not s:
1195 print(
1196 f"Error: Step index {step_index} not found in session.",
1197 file=sys.stderr,
1198 )
1199 raise typer.Exit(code=1)
1200
1201 step_out_file = data.get_step_output_file(step_index)
1202 step_output_str: str | None = None
1203 if step_out_file:
1204 step_output_str = step_out_file.read_text(encoding="utf-8", errors="replace")
1205
1206 trigger = find_triggering_step(data, step_index)
1207 parent_idx: int | None = None
1208 parent_tools: list[dict[str, Any]] = []
1209 if trigger:
1210 parent_idx, parent_step = trigger
1211 parent_tools = parent_step.get("tool_calls", [])
1212
1213 source = s.get("source", "")
1214 tp = s.get("type", "")
1215 created_at = s.get("created_at", "")
1216 status = s.get("status", "")
1217
1218 print(f"### Step {step_index} Details\n")
1219 print(f"- **Source**: `{source}`")
1220 print(f"- **Type**: `{tp}`")
1221 print(f"- **Status**: `{status}`")
1222 print(f"- **Time**: `{created_at}`")
1223 if trigger and parent_idx is not None:
1224 tool_names_str = ", ".join(
1225 f"`{tc.get('name', 'tool')}`" for tc in parent_tools
1226 )
1227 print(f"- **Result of**: Step {parent_idx} ({tool_names_str})")
1228 print()
1229
1230 if trigger and parent_idx is not None:
1231 print(f"#### Triggering Action (Step {parent_idx})")
1232 for tc in parent_tools:
1233 tname = tc.get("name", "unknown")
1234 targs = tc.get("args", {})
1235 action = targs.get("toolAction") or targs.get("toolSummary") or ""
1236 print(f"- **Tool**: `{tname}`")
1237 if action:
1238 print(f" - **Action**: {action}")
1239 if "CommandLine" in targs:
1240 print(
1241 f" - **Command**: `{targs['CommandLine']}` (cwd: `{targs.get('Cwd', '')}`)"
1242 )
1243 elif "AbsolutePath" in targs:
1244 print(f" - **Path**: `{targs['AbsolutePath']}`")
1245 elif "TargetFile" in targs:
1246 print(f" - **File**: `{targs['TargetFile']}`")
1247 print()
1248
1249 content = s.get("content")
1250 if content:
1251 print("#### Content")
1252 print("```")
1253 print(content)
1254 print("```\n")
1255
1256 thinking = s.get("thinking")
1257 if thinking:
1258 print("#### Thinking")
1259 print("```")
1260 print(thinking)
1261 print("```\n")
1262
1263 tool_calls = s.get("tool_calls", [])
1264 if tool_calls:
1265 print("#### Tool Calls")
1266 for tc_idx, tc in enumerate(tool_calls, 1):
1267 name = tc.get("name", "unknown")
1268 args = tc.get("args", {})
1269 print(f"**Tool Call #{tc_idx}: `{name}`**")
1270 print("```json")
1271 print(json.dumps(args, indent=2))
1272 print("```\n")
1273
1274 if step_output_str is not None and step_out_file:
1275 is_duplicate = bool(content and step_output_str.strip() in content.strip())
1276 if not is_duplicate:
1277 lines = step_output_str.splitlines()
1278 truncated = False
1279 if len(lines) > max_lines:
1280 display_text = (
1281 "\n".join(lines[:max_lines])
1282 + f"\n\n... [truncated {len(lines) - max_lines} lines; use --lines to show more] ..."
1283 )
1284 truncated = True
1285 else:
1286 display_text = step_output_str
1287 print(
1288 f"#### Step Output (`{step_out_file.name}`)"
1289 + (" *(truncated)*" if truncated else "")
1290 )
1291 print("```")
1292 print(display_text)
1293 print("```\n")
1294
1295
Audit & Raw Data Lines 1296–1371

16. Specialized Subcommands: tools, commands, raw

Continuing the single-session audit suite, this module defines cmd_tools, cmd_commands, and cmd_raw.

Subcommand Overview:

  • cmd_tools: Invoked via agy-brain-explorer.py <SESSION> tools to generate the complete tool call audit table.
  • cmd_commands: Invoked via agy-brain-explorer.py <SESSION> commands to list all shell executions.
  • cmd_paths: Invoked via agy-brain-explorer.py <SESSION> paths to list all on-disk component locations with clickable file:// links.
  • cmd_compare: Invoked via agy-brain-explorer.py <SESSION> compare <OTHER_SESSION> to execute a differential comparison against another session.
  • cmd_raw: Dumps the exact raw JSON record of any step index formatted with 2-space indentation. Features a --full / --compact switch allowing users to compare the truncated record from transcript.jsonl with the untruncated record in transcript_full.jsonl.
agy-brain-explorer.py Lines 1296–1371
1296@session_app.command(name="tools")
1297def cmd_tools(
1298 ctx: typer.Context,
1299 as_markdown: Annotated[
1300 bool,
1301 typer.Option(
1302 "--markdown",
1303 "--md",
1304 "-m",
1305 help="Output results as Markdown formatted content.",
1306 ),
1307 ] = True,
1308) -> None:
1309 """List all tool calls executed throughout the entire session as Markdown."""
1310 data: SessionData = ctx.obj
1311 show_tools(data)
1312
1313
1314@session_app.command(name="commands")
1315def cmd_commands(
1316 ctx: typer.Context,
1317 as_markdown: Annotated[
1318 bool,
1319 typer.Option(
1320 "--markdown",
1321 "--md",
1322 "-m",
1323 help="Output results as Markdown formatted content.",
1324 ),
1325 ] = True,
1326) -> None:
1327 """List all shell commands run via run_command with details and working directories as Markdown."""
1328 data: SessionData = ctx.obj
1329 show_commands(data)
1330
1331
1332@session_app.command(name="raw")
1333def cmd_raw(
1334 ctx: typer.Context,
1335 step_index: Annotated[
1336 int,
1337 typer.Argument(help="Step index to dump."),
1338 ],
1339 full: Annotated[
1340 bool,
1341 typer.Option(
1342 "--full/--compact",
1343 help="Use transcript_full vs compact transcript.",
1344 ),
1345 ] = True,
1346 as_markdown: Annotated[
1347 bool,
1348 typer.Option(
1349 "--markdown",
1350 "--md",
1351 "-m",
1352 help="Output results as Markdown formatted content.",
1353 ),
1354 ] = True,
1355) -> None:
1356 """Print the raw JSON record of a specific step in a Markdown code block."""
1357 data: SessionData = ctx.obj
1358
1359 lookup = data.full_steps_by_index if full else data.steps_by_index
1360 s = lookup.get(step_index)
1361 if not s:
1362 print(f"Error: Step {step_index} not found.", file=sys.stderr)
1363 raise typer.Exit(code=1)
1364
1365 json_str = json.dumps(s, indent=2)
1366 print(f"### Raw Step {step_index} Record\n")
1367 print("```json")
1368 print(json_str)
1369 print("```")
1370
1371
Session Forensic Search Lines 1372–1432

17. Intra-Session Search: cmd_session_search

When auditing a long trajectory (e.g. 292 steps), manually paging through timeline steps is time-consuming. Developers often need to know: "In which step did the agent run tests with node --test?" or "Where did the agent modify build-docs-site.js?"

cmd_session_search() implements scoped in-session searching across all conversation turns, model reasoning, and tool calls.

Session Search Mechanics:

  • Usage: uv run agy-brain-explorer.py <SESSION> search <QUERY>.
  • Transcript Flexibility: Defaults to searching data.full_steps (via --full), with an optional --compact flag to search compact records.
  • Structured Excerpt Reporting: Evaluates each step against step_matches_query(), printing a Markdown table with the exact step number, timestamp, step source (MODEL, USER_EXPLICIT), type, and matching excerpt.
  • Targeted Forensic Jumping: Once matching step numbers are discovered (e.g. Step 107), developers can immediately drill down with agy-brain-explorer.py <SESSION> step 107 to inspect complete parameters and outputs.
agy-brain-explorer.py Lines 1372–1432
1372@session_app.command(name="search")
1373def cmd_session_search(
1374 ctx: typer.Context,
1375 query: Annotated[
1376 str,
1377 typer.Argument(help="Search query text or keyword within this session."),
1378 ],
1379 full: Annotated[
1380 bool,
1381 typer.Option(
1382 "--full/--compact",
1383 help="Search in full transcript vs compact transcript.",
1384 ),
1385 ] = True,
1386 limit: Annotated[
1387 int | None,
1388 typer.Option(
1389 "--limit",
1390 "-n",
1391 help="Limit number of matching steps displayed.",
1392 ),
1393 ] = None,
1394) -> None:
1395 """Search for matching steps, tool invocations, or content within this session."""
1396 data: SessionData = ctx.obj
1397 steps_to_search = data.full_steps if full else data.steps
1398
1399 matches: list[tuple[int, str, str, str, str]] = []
1400 for s in steps_to_search:
1401 matched, desc = step_matches_query(s, query)
1402 if matched:
1403 idx = s.get("step_index", 0)
1404 tp = s.get("type", "")
1405 source = s.get("source", "")
1406 time_str = ""
1407 with contextlib.suppress(ValueError, TypeError):
1408 time_str = datetime.fromisoformat(
1409 s.get("created_at", "")
1410 ).strftime("%H:%M:%S")
1411 matches.append((idx, time_str, source, tp, desc))
1412
1413 print(
1414 f"### Search Results in Session `{data.session_id}` for \"{query}\" ({len(matches)} matching steps)\n"
1415 )
1416 if not matches:
1417 print("*No matching steps found.*")
1418 return
1419
1420 items_to_show = matches[:limit] if limit else matches
1421 print("| Step | Time | Source | Type | Matching Excerpt |")
1422 print("| :---: | :--- | :--- | :--- | :--- |")
1423 for idx, time_str, source, tp, desc in items_to_show:
1424 clean_desc = sanitize_md_cell(desc)
1425 print(f"| {idx} | {time_str} | {source} | {tp} | {clean_desc} |")
1426
1427 if limit and len(matches) > limit:
1428 print(
1429 f"\n*Showing {len(items_to_show)} of {len(matches)} matching steps.*"
1430 )
1431
1432
Application Architecture Lines 1433–1514

18. Unified CLI Dispatcher & Script Entrypoint

Typer natively expects a single application root. However, agy-brain-explorer.py supports two distinct interaction paradigms:
1) Trace Explorer Mode (discovering all sessions, executing global search, multi-session path listing, or side-by-side comparison via search, compare, paths), and
2) Session Inspection Mode (taking a session argument followed by subcommands like summary, paths, steps, step, tools, commands, compare, raw, or search).

This final section implements a smart CLI router (is_explorer_invocation() and main()) that seamlessly bridges both Typer applications into a unified binary.

Routing Mechanism & Multi-Mode Integration:

  • is_explorer_invocation(args):

    Parses command-line arguments, skipping option flags and their values (including --query, -q, --search, -s). If no positional argument is found, or if the first positional argument matches an explorer keyword (list, explore, traces, search, compare, paths), it routes execution to explorer_app.

  • Command & Search Routing Resolution:
    • If invoked as agy-brain-explorer.py compare <S1> <S2> or agy-brain-explorer.py paths <S1>..., it routes to explorer_app.
    • If invoked as agy-brain-explorer.py <SESSION> compare <S2> or agy-brain-explorer.py <SESSION> paths, <SESSION> is detected as the positional target, routing to session_app.
    • If invoked as agy-brain-explorer.py search <QUERY>, the argument is preserved and routed to explorer_app's cmd_explorer_search().
    • If invoked as agy-brain-explorer.py <SESSION> search <QUERY>, <SESSION> routes to session_app's cmd_session_search().
    • If invoked with -q <QUERY> or --query <QUERY>, it routes to explorer_app's callback, executing prompt-filtered discovery.
  • Default Subcommand Injection: If in session mode and the user only specified a session ID (e.g. agy-brain-explorer.py 4c6b51ee), it automatically injects summary before delegating to session_app().
  • Standard Python Idiom: Guarded by if __name__ == "__main__": main().
agy-brain-explorer.py Lines 1433–1514
1433# ---------------------------------------------------------------------------
1434# CLI Routing Entrypoint
1435# ---------------------------------------------------------------------------
1436
1437
1438def is_explorer_invocation(args: list[str]) -> bool:
1439 """Determine whether the invocation targets the trace explorer or a single session."""
1440 explorer_cmds = {"list", "explore", "traces", "search"}
1441 val_flags = {
1442 "--limit",
1443 "-n",
1444 "--brain",
1445 "-b",
1446 "--offset",
1447 "--lines",
1448 "-l",
1449 "--query",
1450 "-q",
1451 "--search",
1452 "-s",
1453 }
1454 non_opts: list[str] = []
1455 skip_next = False
1456
1457 for a in args:
1458 if skip_next:
1459 skip_next = False
1460 continue
1461 if a in val_flags:
1462 skip_next = True
1463 continue
1464 if not a.startswith("-"):
1465 non_opts.append(a)
1466
1467 if not non_opts:
1468 # No arguments or only flags (e.g. `agy-brain-explorer.py` or `--md`)
1469 return True
1470
1471 return non_opts[0] in explorer_cmds
1472
1473
1474def main() -> None:
1475 """Unified entrypoint that routes between trace explorer and session inspection."""
1476 args = sys.argv[1:]
1477 help_flags = {"-h", "--help"}
1478
1479 if is_explorer_invocation(args):
1480 # Explorer mode
1481 alias_cmds = {"list", "explore", "traces"}
1482 filtered_args: list[str] = []
1483 stripped_cmd = False
1484 for a in args:
1485 if not stripped_cmd and a in alias_cmds:
1486 stripped_cmd = True
1487 continue
1488 filtered_args.append(a)
1489 sys.argv = [sys.argv[0]] + filtered_args
1490 explorer_app()
1491 else:
1492 # Session inspection mode
1493 session_subcommands = {
1494 "summary",
1495 "steps",
1496 "step",
1497 "tools",
1498 "commands",
1499 "raw",
1500 "search",
1501 }
1502 has_subcommand = any(arg in session_subcommands for arg in args)
1503 final_args = list(args)
1504 if not has_subcommand and not any(arg in help_flags for arg in args):
1505 for i, arg in enumerate(final_args):
1506 if not arg.startswith("-"):
1507 final_args.insert(i + 1, "summary")
1508 break
1509 sys.argv = [sys.argv[0]] + final_args
1510 session_app()
1511
1512
1513if __name__ == "__main__":
1514 main()