Skip to content

feat(agent-manager): add Gemini CLI agent adapter#70

Merged
codeaholicguy merged 1 commit intocodeaholicguy:mainfrom
nnhhoang:feat/gemini-cli-adapter
Apr 22, 2026
Merged

feat(agent-manager): add Gemini CLI agent adapter#70
codeaholicguy merged 1 commit intocodeaholicguy:mainfrom
nnhhoang:feat/gemini-cli-adapter

Conversation

@nnhhoang
Copy link
Copy Markdown
Contributor

Adds a GeminiCliAdapter so ai-devkit agent list and ai-devkit agent detail can discover and inspect running Gemini CLI sessions. The adapter implements the existing AgentAdapter contract (canHandle / detectAgents / getConversation) and registers in every agent.ts entrypoint that composes an AgentManager.

Gemini-specific design

Process detection. Gemini CLI ships as a Node script — bundle/gemini.js with shebang #!/usr/bin/env node. On POSIX systems ps aux therefore reports the process as node /path/to/gemini ... with argv[0] = node, not gemini. Empirically verified on macOS + Volta:

node /Users/foo/.volta/tools/image/node/24.14.0/bin/gemini --help

The adapter reflects this: it requests the Node process pool via listAgentProcesses('node') and keeps only entries whose command line references a gemini entrypoint (gemini, gemini.exe, gemini.js basename in any token). This is the only place the adapter deviates from the argv[0]-basename pattern, and the rationale is documented in JSDoc on both detectAgents and isGeminiExecutable.

Session discovery. Sessions live at ~/.gemini/tmp/<shortId>/chats/session-*.json. <shortId> is opaque (managed by Gemini's project registry), so the adapter iterates every short-id directory and filters by matching session.projectHash — sha256 of the project root Gemini resolved at write time via its .git-bounded findProjectRoot walk. To stay consistent with that walk, the adapter hashes every ancestor of each process CWD as a candidate (candidateProjectRoots) so subdirectory invocations still line up with the session file the Gemini process wrote. Matched sessions populate resolvedCwd on a SessionFile, then the shared matchProcessesToSessions() performs the standard CWD + birthtime 1:1 greedy assignment. Unmatched processes fall back to the process-only AgentInfo shape.

Session parsing. Gemini writes a single JSON object per file (not JSONL) with schema { sessionId, projectHash, startTime, lastUpdated, messages[], directories?, kind }. Each message entry has type of user, gemini, thought, or tool — verified against the vendored bundle source. Visible turns map to user/assistant roles; thought and tool entries are hidden by default and surface as system when --verbose is passed. displayContent takes priority over content when both are present.

Test coverage

37 unit tests:

  • canHandle: plain command, full path (case-insensitive), non-match, path-argument false positive, Node-invoked gemini (real Volta install shape), Node-invoked gemini.js bundle entrypoint
  • detectAgents: empty, filters non-gemini Node processes out of the pool, process-only fallback, matched-session mapping, cross-project isolation
  • discoverSessions: missing ~/.gemini/tmp, empty CWD, projectHash mismatch, malformed JSON skip, session- filename prefix enforcement, parent-of-cwd git-root fallback
  • determineStatus: waiting for gemini/assistant entries, running for user, idle past threshold
  • parseSession: valid, cached content bypass, missing file, invalid JSON, missing sessionId, empty-summary default, 120-char truncation, lastUpdated priority over entry timestamp
  • getConversation: user/assistant routing, verbose system role, missing/malformed files, displayContent priority, empty-content skip, missing type, non-array messages

nx run-many -t build test lint passes — 461 tests across 4 packages, 0 lint errors. Also verified end-to-end by spawning a Node script whose basename resolves to gemini, writing a synthetic session JSON, and calling adapter.detectAgents() directly: process is detected, matched to its session via projectHash, and mapped to an AgentInfo with status, summary, and sessionFilePath populated.

CLI wiring

Registered in all four agent.ts entrypoints (list, detail, stop, focus). The agent detail route map also maps gemini_cli → the new adapter for conversation rendering.

@codeaholicguy
Copy link
Copy Markdown
Owner

Did you test the implementation? I checked out the code, but the behavior when running agent list is not as expected; there is no Gemini session even though I already have one running.

@codeaholicguy
Copy link
Copy Markdown
Owner

codeaholicguy commented Apr 21, 2026

I also suggest that you run this work with dev-lifecycle so that we have the artifact (design, implementation) of this new integration in docs/ai for referencing later.

Comment thread packages/agent-manager/src/adapters/GeminiCliAdapter.ts Outdated
Adds a GeminiCliAdapter alongside the existing Claude Code and Codex
adapters so `ai-devkit agent list` and `ai-devkit agent detail` can
discover and inspect running Gemini CLI sessions. The adapter follows
the canHandle / detectAgents / getConversation contract and registers
in every agent.ts entrypoint that composes an AgentManager.

Process detection enumerates `node` processes and filters argv for a
`gemini` / `gemini.exe` / `gemini.js` basename, because Gemini CLI is
distributed as a pure Node script (unlike Claude's native binary or
Codex's Node wrapper around Rust). Earlier Windows-specific basename
handling was removed to stay close to the existing adapter pattern.

Session discovery walks `~/.gemini/tmp/<shortId>/chats/session-*.json`
across every project short-id directory Gemini maintains. Each
session JSON carries its own `projectHash` — sha256 of the project
root that Gemini CLI resolved at write time via its `.git`-bounded
walk. The adapter enumerates every ancestor of each running process'
CWD, hashes each candidate, and matches on projectHash to populate
`resolvedCwd` on a SessionFile. The shared
`matchProcessesToSessions()` performs the usual CWD + birthtime 1:1
greedy assignment, and processes without a matching session fall
back to the process-only AgentInfo shape.

`getConversation` parses Gemini's single-JSON-per-file layout (not
JSONL): `messages` is an array with `type` of 'user' or 'gemini' for
visible turns; 'thought' and 'tool' entries are hidden by default and
surface as `system` role under `--verbose`. Message `content` is
polymorphic — assistant messages store a string, user messages store
`Part[]` (e.g. `[{text: '...'}]`). A `resolveContent` helper
normalizes both shapes so `.trim()` never runs on an array, and
`displayContent` takes priority over `content` when both are present.

Test coverage mirrors CodexAdapter — 42 unit tests across
initialization, canHandle, detectAgents, discoverSessions (including
parent-of-cwd git-root cases), determineStatus, parseSession
(string + array content + non-text parts), and getConversation. Also
updates the jest mock in the CLI agent command test so `agent detail`
can route `gemini_cli` agents through the new adapter.

Docs follow the repo's dev-lifecycle skill: `docs/ai/{requirements,
design,planning,implementation,testing}/feature-gemini-cli-adapter.md`
capture the Node-script distribution rationale, session schema,
projectHash algorithm, content-normalization decisions, and the
real-Gemini end-to-end verification.
@nnhhoang nnhhoang force-pushed the feat/gemini-cli-adapter branch from 331369d to b54a981 Compare April 22, 2026 03:26
@codeaholicguy
Copy link
Copy Markdown
Owner

LGTM.

Thank you for your contribution.

@codeaholicguy codeaholicguy merged commit 81d4f7b into codeaholicguy:main Apr 22, 2026
7 checks passed
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