feat(enhancement): add antigravity-cli provider#1234
Conversation
Integrates google-antigravity/antigravity-cli (binary: agy) as an ACP-based provider following the recommended integration pattern. - packages/protocol: add 'antigravity' to AGENT_PROVIDER_DEFINITIONS - packages/server: add AntigravityACPAgentClient (ACP, defaultCommand: agy --acp) with getDiagnostic() support; register factory in PROVIDER_CLIENT_FACTORIES - packages/app: add AntigravityIcon SVG component; register in BUILTIN_PROVIDER_ICONS and BuiltinProviderIconName - packages/server/daemon-e2e: add antigravity to agentConfigs, isProviderAvailable, and allProviders The provider delegates auth to the agy CLI (Google Sign-In), matching the pattern used by Copilot. Modes are discovered dynamically via ACP at runtime; no static mode IDs are assumed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
agy does not support --acp. Replace the ACP stub with a direct provider that uses agy's --print flag for non-interactive single-turn execution. Key implementation details: - AntigravityAgentClient implements AgentClient directly (no ACP) - AntigravityAgentSession spawns agy with stdio:['ignore','pipe','pipe'] (ignore stdin prevents agy from hanging waiting for EOF) - Session continuity: new conversation UUID discovered from new .pb file at ~/.gemini/antigravity-cli/conversations/ after first turn - Follow-up turns use --conversation <uuid> to resume the session - Two modes: 'default' (ask) and 'bypass' (--dangerously-skip-permissions) - --add-dir cwd only passed in bypass mode to avoid permission hangs in default mode when large directories need indexing - provider-manifest.ts: add real modes (default/bypass) with defaultModeId - agent-configs.ts: add modes.full='bypass', modes.ask='default' Verified: isAvailable(), createSession(), run(), session continuity, describePersistence(), and follow-up turns all work end-to-end. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adopted two techniques from openab/agy-acp: 1. Delta extraction (fixes bug: follow-up turns previously emitted the full conversation history, not just the new response) agy --conversation <uuid> --print replays all prior turn output before the new response. Track _prevOutput per session and strip it as a prefix on every turn, mirroring the extract_delta() approach in openab/agy-acp/src/main.rs. If agy's output is not append-only (prefix strip fails), log a warning and reset the baseline. 2. Multi-file guard on conversation discovery If two agy sessions run concurrently, multiple .pb files may appear in the snapshot diff. Refuse to bind the conversation ID in that case (same guard as openab) rather than picking an arbitrary file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace Array.toReversed() with .slice().reverse() in agent-manager.ts and agent-timeline-store.ts (toReversed requires Node.js 20.x+) - Add antigravity.real.e2e.test.ts: 4 e2e tests using in-process daemon harness (provider discovery, basic prompt, session continuity, identity) - All 4 tests pass against live agy binary Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…arget) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…structured logging Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
| Filename | Overview |
|---|---|
| packages/server/src/server/agent/providers/antigravity-agent.ts | Core provider implementation — spawns agy --print, strips replayed history via prefix-diff, detects new conversation IDs by diffing the conversations directory; previously-flagged stderr drain and mismatch handling are now addressed, but updateSettingsModel has an unguarded read-modify-write race on the shared settings.json when concurrent sessions use different models. |
| packages/server/src/server/daemon-e2e/agent-configs.ts | Adds antigravity to the shared test config map; contains a stray /** line immediately before the existing JSDoc block for forEachProvider, producing a garbled comment. |
| packages/protocol/src/provider-manifest.ts | Adds Antigravity provider definition and its two modes (default / bypass) to AGENT_PROVIDER_DEFINITIONS; description text has already been flagged in a prior review thread for advertising MCP support while capabilities declare supportsMcpServers: false. |
| packages/server/src/server/agent/provider-registry.ts | Registers AntigravityAgentClient in the provider factory map — straightforward wiring with no issues. |
| packages/server/src/server/daemon-e2e/antigravity.real.e2e.test.ts | Real e2e tests covering availability, basic prompt, session continuity, and model identity; uses return to skip when agy is unavailable (tests appear "passed" rather than "skipped" in CI, but this is a pre-existing Vitest pattern used elsewhere in the project). |
| packages/app/src/components/icons/antigravity-icon.tsx | New SVG icon component for Antigravity using react-native-svg; straightforward and matches the pattern of other provider icons in the codebase. |
| packages/app/src/components/provider-icon-name.ts | Adds "antigravity" to BuiltinProviderIconName union type and BUILTIN_PROVIDER_IDS set — correct and consistent with the existing pattern. |
| packages/app/src/components/provider-icons.ts | Maps the "antigravity" id to AntigravityIcon in BUILTIN_PROVIDER_ICONS; uses the same as unknown as ProviderIconComponent cast pattern as other icons. |
Sequence Diagram
sequenceDiagram
participant UI as App UI
participant Reg as ProviderRegistry
participant Sess as AntigravityAgentSession
participant FS as settings.json
participant AGY as agy (child process)
UI->>Reg: createSession(config)
Reg->>Sess: "new AntigravityAgentSession(conversationId=null)"
UI->>Sess: startTurn(prompt)
Sess->>FS: updateSettingsModel(model) [sync read-modify-write]
Sess->>Sess: snapshot preConversations
Sess->>AGY: spawn("agy", ["--conversation", id?, "--print", prompt])
Sess-->>UI: emit turn_started
loop stdout lines
AGY-->>Sess: line
Sess->>Sess: prefix-diff against _prevOutput
alt new content
Sess-->>UI: emit timeline (assistant_message)
else mismatch
Sess->>Sess: "mismatchDetected = true"
end
end
AGY-->>Sess: close(code)
alt first turn (no conversationId)
Sess->>Sess: diff postConversations → bind new ID
end
alt mismatchDetected
Sess-->>UI: emit turn_failed
else "code === 0"
Sess->>Sess: "_prevOutput = fullOutput"
Sess-->>UI: emit turn_completed
else "code !== 0"
Sess-->>UI: emit turn_failed
end
Reviews (3): Last reviewed commit: "Merge branch 'upstream/main' into featur..." | Re-trigger Greptile
- Drain proc.stderr to prevent pipe buffer deadlock (64KB OS limit) - Suppress streaming emission on delta mismatch; emit turn_failed at close instead of broadcasting replayed conversation history - Remove 'MCP support' from provider description (supportsMcpServers: false) - Fix install diagnostic: use official curl script, not a made-up npm package Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ved by Gemini 3.5 Flash)
| function updateSettingsModel(model: string, logger: Logger) { | ||
| const settingsPath = join(homedir(), ".gemini", "antigravity-cli", "settings.json"); | ||
| try { | ||
| let settings: AntigravitySettings = {}; | ||
| if (existsSync(settingsPath)) { | ||
| settings = JSON.parse(readFileSync(settingsPath, "utf8")) as AntigravitySettings; | ||
| } | ||
| if (settings.model !== model) { | ||
| settings.model = model; | ||
| writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf8"); | ||
| logger.info({ model }, "antigravity: updated model in settings.json"); | ||
| } | ||
| } catch (err) { | ||
| logger.error({ err }, "antigravity: failed to update settings.json model"); | ||
| } | ||
| } |
There was a problem hiding this comment.
updateSettingsModel is not concurrency-safe across sessions with different models
updateSettingsModel follows a read → compare → write sequence using synchronous Node.js file I/O with no locking. If two AntigravityAgentSession instances start turns concurrently — e.g., user switches to Antigravity in two windows, one with "Gemini 3.5 Flash (High)" and one with "Gemini 3.1 Pro (Low)" — both processes can race through the read and land on different write calls. The last writer wins and overwrites the other session's model. The agy process for the first session then picks up the wrong model from settings.json at startup, silently running an entirely different model than the user selected.
Consider moving the model into a per-invocation CLI flag (if agy exposes one) rather than mutating the shared global settings.json, or at minimum serialising access through a file lock or a module-level async queue.
|
agy is quite generous in quota... Looking forward to this!!! |
Linked issue
Closes #1131
Type of change
What does this PR do
Added antigravity-cli as a provider to paseo. Since
agy --acpdoes not exists, key changes are: passingagy --print "prompt", which runs non-interactively and writes the response to stdout. And add antigravity as a registered provider.How did you verify it
Tested locally (Linux x86_64) that antigravity works:

Checklist
npm run typecheckpassesnpm run lintpassesnpm run formatran (Biome)