Skip to content

feat: AGY (Antigravity) provider — with threading + full type coverage#243

Open
iansherr wants to merge 3 commits into
daaain:mainfrom
iansherr:feat/agy-provider
Open

feat: AGY (Antigravity) provider — with threading + full type coverage#243
iansherr wants to merge 3 commits into
daaain:mainfrom
iansherr:feat/agy-provider

Conversation

@iansherr

@iansherr iansherr commented Jun 25, 2026

Copy link
Copy Markdown

Depends on #242 (base abstraction). Adds the AGY (Antigravity) provider as the second fully-realized adapter.

What's included

  • AgyProvider — discovers and loads sessions from ~/.gemini/antigravity-cli/brain/
  • Entry threading — sequential parentUuid chaining creates a proper DAG instead of flat entries
  • Full entry type coverage — all AGY transcript types handled:
    • USER_INPUT → user message (extracts <USER_REQUEST> content)
    • PLANNER_RESPONSE → assistant message + tool calls
    • CHECKPOINT → compaction summaries
    • LIST_DIRECTORY → tool output
    • GENERIC → uncategorized model output
    • RUN_COMMAND → shell command executions
    • VIEW_FILE → file reads
    • CODE_ACTION → code modifications (edits, writes)
  • Pyright: 0 errors, 0 warnings (strict mode)
  • All 2207 tests pass

What's not included (coming as separate PRs)

  • Codex, OpenCode, Gemini adapters
  • --provider CLI flag wiring
  • --all-providers path filtering composition

Verification

No behavior change to existing functionality — AGY is purely additive. The provider follows the same BaseProvider ABC established in #242.

Summary by CodeRabbit

  • New Features
    • Added unified session discovery across multiple providers, with optional filtering by provider.
    • Added centralized session loading with automatic provider selection and provider availability checks.
    • Introduced a provider abstraction layer to support multiple transcript formats.
    • Added support for Claude and Agy session formats, including parsing of user/assistant/tool interactions.
    • Exposes session metadata (e.g., title/timestamps) and supports limiting loaded messages for quicker browsing.

Sisyphus added 2 commits June 25, 2026 16:09
- Add AgyProvider for Antigravity CLI sessions
- Entry threading via parentUuid chaining (sequential DAG)
- Handle all AGY entry types: USER_INPUT, PLANNER_RESPONSE, CHECKPOINT,
  LIST_DIRECTORY, GENERIC, RUN_COMMAND, VIEW_FILE, CODE_ACTION
- Register in ProviderRegistry for auto-discovery
- Pyright: 0 errors (strict mode)
- All 2207 tests pass
@iansherr

Copy link
Copy Markdown
Author

Note: this PR builds on #242 (base abstraction + Claude adapter). It's meant as an add-on — the base abstraction should land first, then this adds AGY as the second fully-realized provider on top of it.

The diff is against main so it includes the base abstraction files; once #242 merges, the AGY-only diff will be clean.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds shared provider contracts, Claude and Agy session providers, a registry for discovery and loading, and a top-level module for aggregated session iteration and stats.

Changes

Provider session discovery

Layer / File(s) Summary
Shared provider contract
claude_code_log/providers/base.py
Defines SessionInfo, transcript entry builders, content normalization helpers, and the abstract provider interface.
Claude provider
claude_code_log/providers/claude.py
Adds Claude session discovery under ~/.claude/projects and loads sessions from matching JSONL transcripts.
Agy discovery and loading
claude_code_log/providers/agy.py
Adds Agy provider identity, data-directory lookup, session discovery, JSONL session loading, and transcript entry dispatch.
Agy transcript parsing
claude_code_log/providers/agy.py
Adds parsing for user input, planner responses, checkpoints, generic output, commands, file views, code actions, tool calls, and user-request extraction.
Registry and public API
claude_code_log/providers/registry.py, claude_code_log/discovery.py, claude_code_log/providers/__init__.py
Adds provider registration, availability-aware discovery/loading across providers, the unified discovery facade, and package re-exports.

Sequence Diagram(s)

Discovery flow:

sequenceDiagram
  participant Client
  participant discovery_py as discovery.py
  participant ProviderRegistry
  participant ClaudeProvider
  participant AgyProvider

  Client->>discovery_py: discover_all_sessions()
  discovery_py->>ProviderRegistry: discover_providers()
  discovery_py->>ProviderRegistry: discover_all_sessions()
  ProviderRegistry->>ClaudeProvider: discover_sessions()
  ProviderRegistry->>AgyProvider: discover_sessions()
Loading

Session load flow:

sequenceDiagram
  participant Client
  participant discovery_py as discovery.py
  participant ProviderRegistry
  participant ClaudeProvider
  participant AgyProvider

  Client->>discovery_py: load_session(provider_name, session_id)
  discovery_py->>ProviderRegistry: load_session(provider_name, session_id)
  alt provider_name == claude
    ProviderRegistry->>ClaudeProvider: load_session(session_id)
  else provider_name == agy
    ProviderRegistry->>AgyProvider: load_session(session_id)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through logs with a twitchy nose,
From Claude to Agy, the session grows.
The registry carrot is neat and sweet,
Now every trail has a tidy beat.
Huzzah! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding the AGY provider with threading and broad transcript type support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
claude_code_log/providers/registry.py (1)

29-36: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Silent broad-except hides provider init failures.

Swallowing every Exception with a bare pass makes a misconfigured or buggy provider silently disappear with no diagnostic trail, which is hard to debug in the field. Consider logging the failure (provider name + exception) at warning level.

♻️ Proposed change
+import logging
+
+logger = logging.getLogger(__name__)
+
     def instantiate_registered(self) -> None:
-        for provider_class in self._provider_classes.values():
+        for name, provider_class in self._provider_classes.items():
             try:
                 provider = provider_class()
                 self.register(provider)
-            except Exception:
-                # Skip providers that fail to initialize
-                pass
+            except Exception:
+                logger.warning("Provider %r failed to initialize", name, exc_info=True)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude_code_log/providers/registry.py` around lines 29 - 36, The
instantiate_registered method is swallowing all provider initialization failures
with a bare except and pass, which hides misconfigured providers. Update this
loop in Registry.instantiate_registered to catch the failure, log a warning that
includes the provider_class name and the exception details, and then continue
skipping that provider instead of silently ignoring it.
claude_code_log/discovery.py (3)

21-29: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicates ProviderRegistry.discover_all_sessions.

When providers is None, this loop re-implements logic already in registry.discover_all_sessions() (Lines 54-58 of registry.py), including the is_available() gate. Consider delegating to the registry to avoid divergence, e.g. fall back to registry.discover_all_sessions() for the None case and only filter by name when an explicit list is given.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude_code_log/discovery.py` around lines 21 - 29, Replace the duplicated
discovery logic in discover_sessions so the providers is None path delegates to
ProviderRegistry.discover_all_sessions instead of re-implementing the
availability check and iteration. Keep the explicit providers filter path using
discover_providers() and registry.get_provider(provider_name), but for the
default case call the registry method directly to stay aligned with
ProviderRegistry.discover_all_sessions and avoid divergence.

45-55: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Re-discovering providers on every facade call.

Each of discover_all_sessions, discover_sessions_by_provider, get_session_stats, and load_session calls discover_providers(), which re-instantiates all providers and re-runs is_available() filesystem checks. For callers invoking these in sequence this repeats work; consider accepting an optional pre-built ProviderRegistry or memoizing discovery.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude_code_log/discovery.py` around lines 45 - 55, The session discovery
helpers are repeatedly rebuilding provider state by calling discover_providers()
inside each facade function, which re-runs availability checks and duplicates
work; update discover_all_sessions, discover_sessions_by_provider,
get_session_stats, and load_session to reuse a shared ProviderRegistry, either
by accepting an optional registry parameter or by memoizing the discovery
result. Use the existing discover_providers, ProviderRegistry, and related
helper names to centralize provider lookup instead of instantiating providers on
every call.

58-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

max_messages not exposed through the facade.

ProviderRegistry.load_session (and the underlying provider) accept max_messages, but this public entrypoint drops it, so callers cannot bound transcript loading. Consider threading it through.

♻️ Proposed change
-def load_session(provider_name: str, session_id: str):
+def load_session(
+    provider_name: str, session_id: str, max_messages: Optional[int] = None
+):
@@
     registry = discover_providers()
-    return registry.load_session(provider_name, session_id)
+    return registry.load_session(provider_name, session_id, max_messages=max_messages)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude_code_log/discovery.py` around lines 58 - 69, The public load_session
facade in discovery.py drops the max_messages option even though
ProviderRegistry.load_session and the provider implementations support it, so
thread that argument through this entrypoint. Update load_session to accept
max_messages and pass it into registry.load_session alongside provider_name and
session_id, keeping the signature aligned with ProviderRegistry.load_session so
callers can bound transcript loading.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@claude_code_log/providers/agy.py`:
- Around line 96-97: The max_messages check in the Agy transcript reader is
using the JSONL line index from the loop instead of the number of yielded
TranscriptEntry objects, so it does not actually limit emitted messages and is
off by one. Update the logic in the reading path around the loop that builds
TranscriptEntry values so it counts produced entries (not lines) and stops once
max_messages entries have been yielded, using the existing transcript parsing
flow to enforce the limit correctly.
- Around line 77-94: The session loader in the Agy transcript parsing flow stops
on a single bad JSON line because `json.loads` inside the generator is
unguarded. Update the `AGY` parser method that iterates the transcript file to
catch `json.JSONDecodeError`, log the malformed line with session context, and
continue to the next line. Keep yielding valid entries from `_parse_entry` and
preserve `prev_uuid` handling so one corrupt record does not abort the rest of
the session.
- Around line 326-340: The UUID generation in the tool call loop is not unique
when multiple tool calls share the same name within a single entry. Update the
`agy` provider logic in the loop that builds entries with `make_assistant_entry`
so the `uid` includes the per-tool loop position (or another unique per-call
value) in addition to `session_id`, `index`, and `name`, ensuring each
`parentUuid` chain remains unambiguous even when `name` repeats.
- Around line 61-72: `AgyProvider.load_session()` builds the transcript path
directly from caller-supplied `session_id`, which can allow `..` or path
separators to escape the intended `brain/<id>/.../transcript.jsonl` location.
Add validation on `session_id` before composing `transcript_file` so only safe
IDs are accepted, either by rejecting path separators/parent traversal or by
enforcing a strict allowed pattern. Keep the check near the existing path
construction in `load_session()` and fail fast with a clear error if the ID is
invalid.

---

Nitpick comments:
In `@claude_code_log/discovery.py`:
- Around line 21-29: Replace the duplicated discovery logic in discover_sessions
so the providers is None path delegates to
ProviderRegistry.discover_all_sessions instead of re-implementing the
availability check and iteration. Keep the explicit providers filter path using
discover_providers() and registry.get_provider(provider_name), but for the
default case call the registry method directly to stay aligned with
ProviderRegistry.discover_all_sessions and avoid divergence.
- Around line 45-55: The session discovery helpers are repeatedly rebuilding
provider state by calling discover_providers() inside each facade function,
which re-runs availability checks and duplicates work; update
discover_all_sessions, discover_sessions_by_provider, get_session_stats, and
load_session to reuse a shared ProviderRegistry, either by accepting an optional
registry parameter or by memoizing the discovery result. Use the existing
discover_providers, ProviderRegistry, and related helper names to centralize
provider lookup instead of instantiating providers on every call.
- Around line 58-69: The public load_session facade in discovery.py drops the
max_messages option even though ProviderRegistry.load_session and the provider
implementations support it, so thread that argument through this entrypoint.
Update load_session to accept max_messages and pass it into
registry.load_session alongside provider_name and session_id, keeping the
signature aligned with ProviderRegistry.load_session so callers can bound
transcript loading.

In `@claude_code_log/providers/registry.py`:
- Around line 29-36: The instantiate_registered method is swallowing all
provider initialization failures with a bare except and pass, which hides
misconfigured providers. Update this loop in Registry.instantiate_registered to
catch the failure, log a warning that includes the provider_class name and the
exception details, and then continue skipping that provider instead of silently
ignoring it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ce191374-cdf8-43c6-bc44-20d351a8eadd

📥 Commits

Reviewing files that changed from the base of the PR and between 4bd633f and 9f45b64.

📒 Files selected for processing (6)
  • claude_code_log/discovery.py
  • claude_code_log/providers/__init__.py
  • claude_code_log/providers/agy.py
  • claude_code_log/providers/base.py
  • claude_code_log/providers/claude.py
  • claude_code_log/providers/registry.py

Comment thread claude_code_log/providers/agy.py
Comment thread claude_code_log/providers/agy.py
Comment thread claude_code_log/providers/agy.py Outdated
Comment thread claude_code_log/providers/agy.py Outdated
- Path traversal: validate session_id against hex+dash pattern
- Malformed JSON: skip bad lines with logging instead of aborting
- max_messages: count yielded entries, not raw JSONL lines
- Duplicate UUIDs: include tool call index in UUID
@iansherr

Copy link
Copy Markdown
Author

Thanks @coderabbitai — all 4 items addressed:

  • Path traversal: session_id now validated against ^[a-f0-9\-]+$ before path construction
  • Malformed JSON: json.loads wrapped in try/except, bad lines logged and skipped
  • max_messages: now counts yielded TranscriptEntry objects, not raw JSONL lines
  • Duplicate UUIDs: tool call index incorporated into UUID ({session_id}-{index}-{tc_index}-{name})

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@iansherr Thanks for the thorough follow-up — those fixes sound good to me, especially the stricter session_id validation, per-entry max_messages accounting, and the UUID disambiguation for tool calls.

🐇✨


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
claude_code_log/providers/agy.py (1)

43-55: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Filter out session IDs that load_session() will reject.

Line 53 yields any directory name, but Lines 60-61 later reject anything outside [a-f0-9-]+. That means discover_sessions() can return sessions this provider cannot load if brain/ contains any non-session subdirectory with a transcript file. Reuse _is_valid_session_id(session_dir.name) here so discovery and loading stay consistent.

Suggested fix
         for session_dir in brain_dir.iterdir():
             if not session_dir.is_dir():
                 continue
+            if not self._is_valid_session_id(session_dir.name):
+                continue
             transcript_file = (
                 session_dir / ".system_generated" / "logs" / "transcript.jsonl"
             )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude_code_log/providers/agy.py` around lines 43 - 55, discover_sessions()
in the agy provider is returning directories that load_session() will later
reject, so align discovery with loading. In the loop over session_dir entries,
reuse _is_valid_session_id(session_dir.name) before yielding SessionInfo so only
valid session IDs are discovered, keeping the provider’s session listing
consistent with its load rules.
♻️ Duplicate comments (1)
claude_code_log/providers/agy.py (1)

99-107: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Enforce max_messages inside the inner yield loop.

This now counts emitted TranscriptEntry objects, but the limit is still checked only after _parse_entry() finishes. A single PLANNER_RESPONSE with multiple tool calls can therefore emit past the requested cap before Line 107 breaks.

Suggested fix
                     entry = cast(dict[str, Any], raw_entry)
                     for transcript_entry in self._parse_entry(
                         entry, session_id, message_count, prev_uuid
                     ):
+                        if max_messages is not None and message_count >= max_messages:
+                            return
                         if hasattr(transcript_entry, "uuid"):
                             prev_uuid = cast(Any, transcript_entry).uuid
                         yield transcript_entry
                         message_count += 1
-
-                if max_messages is not None and message_count >= max_messages:
-                    break
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude_code_log/providers/agy.py` around lines 99 - 107, The max_messages cap
is only enforced after _parse_entry() finishes, so a single entry can yield too
many TranscriptEntry objects before the loop stops. In the iterator that calls
_parse_entry in agy.py, add the max_messages check inside the inner for-loop
immediately after each yield in the transcript_entry emission path, and break
out as soon as message_count reaches the limit while preserving prev_uuid
updates in the same loop.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@claude_code_log/providers/agy.py`:
- Around line 43-55: discover_sessions() in the agy provider is returning
directories that load_session() will later reject, so align discovery with
loading. In the loop over session_dir entries, reuse
_is_valid_session_id(session_dir.name) before yielding SessionInfo so only valid
session IDs are discovered, keeping the provider’s session listing consistent
with its load rules.

---

Duplicate comments:
In `@claude_code_log/providers/agy.py`:
- Around line 99-107: The max_messages cap is only enforced after _parse_entry()
finishes, so a single entry can yield too many TranscriptEntry objects before
the loop stops. In the iterator that calls _parse_entry in agy.py, add the
max_messages check inside the inner for-loop immediately after each yield in the
transcript_entry emission path, and break out as soon as message_count reaches
the limit while preserving prev_uuid updates in the same loop.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3ccea18d-7d93-45c5-82e5-deb28723c472

📥 Commits

Reviewing files that changed from the base of the PR and between 9f45b64 and c035fb3.

📒 Files selected for processing (1)
  • claude_code_log/providers/agy.py

@cboos

cboos commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

Woops ;-) Sorry, I don't know why I just noticed #242 and not this follow-up! My bad. I'll review them together, hopefully later today.

@iansherr

iansherr commented Jun 28, 2026

Copy link
Copy Markdown
Author

@cboos -- No worries, I appreciate your perspective.

I've been tinkering with making this PR more meaningful by adding cli flags and/or output rendering, but I'm quickly hitting walls that require much bigger changes.

For example, I found that AGY session data wasn't populating the search index (no session previews, no timestamps), and the search result display assumed Claude's URL format. I've identified fixes for these locally, but they touch the CLI data pipeline and the search template, which I respect is a lot to change in one PR. Happy to push an updated version of #243 with these fixes, or split the index/search rendering into a separate follow-up if you'd prefer to keep each PR focused.

Finally, I also noticed a pre-existing cosmetic issue where the <!-- Search Component --> comment is duplicated. The index.html has its own comment, then the included search.html also starts with one. I was getting tripped up for a while trying to figure out if I'd introduced that bug before I finally realized it was preexisting. It doesn't break functionality on Claude indexes, but just to flag.

@cboos

cboos commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

(Claude) Reviewed #243 on top of #242, and re-tested on a real ~500-entry Antigravity session.

Together these two PRs resolve every concrete finding from the earlier review of #225:

  • pyright 0 (was 274), docs symlinks preserved, and the registry's reportPrivateUsage is gone (encapsulated in instantiate_registered()).
  • Entry threading: parentUuid now chains sequentially, so a session builds a single-root DAG instead of the old flat "N roots found (N-1 unexpected)" list.
  • Full 8-type coverage: GENERIC / RUN_COMMAND / VIEW_FILE / CODE_ACTION are parsed instead of silently dropped.
  • session_id is now validated, closing the path-traversal note.

Re-test on the same session that previously lost 19 of 47 entries: 49 → 68 entries, ~49 flat roots → 1 threaded root, and the run_command / view_file / code_action content that was missing now renders. The split (base abstraction + one provider, properly typed and threaded) is exactly the right shape — solid, merge-ready foundation.

Where to take it next. This is a good foundation, but there's still a gap to get agy output really on par with Claude Code's own log rendering: structural fidelity. Right now tool actions render as labeled assistant text ([run_command: …], [view_file: …] blockquotes), even though #242's base already exposes make_tool_use_entry / make_tool_result_entry. The path forward is to emit those so the existing renderers engage:

  1. Generic tool renderer first — map each tool action to a tool_use + tool_result pair so it renders as a proper collapsible card (params table + result), instead of a text blockquote. This alone gets you most of the way for any tool.
  2. Then give the critical tools their real realization by mapping agy's actions onto Claude's tool types: VIEW_FILE → Read, CODE_ACTION → Write/Edit, RUN_COMMAND → Bash. Those pick up claude-code-log's specialized renderers — syntax-highlighted file views, edit diffs, command/output framing — which is what makes the Claude logs actually readable.

That's the step from "all the content is now present" to "reads like a Claude transcript." Nice work on the cleanup and the split.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants