feat(agent): add Antigravity (agy CLI) as a direct-CLI agent - #741
Open
kaizhou-lab wants to merge 20 commits into
Open
feat(agent): add Antigravity (agy CLI) as a direct-CLI agent#741kaizhou-lab wants to merge 20 commits into
kaizhou-lab wants to merge 20 commits into
Conversation
added 18 commits
July 31, 2026 22:32
…type Antigravity is driven through the `agy` CLI, which runs one process per turn with `-p --output-format stream-json`. It is neither an ACP vendor (it does not speak ACP) nor an in-process engine like Aionrs (it does spawn a child process), so it gets its own AgentType instead of reusing `acp`. - `full_auto_mode_id` returns "default": agy's full-auto is the `--dangerously-skip-permissions` flag, not a mode id; its mode axis is default / accept-edits / plan. - `native_skills_dirs` returns `.agents/skills`, verified against agy 1.1.8 — a skill at `.agents/skills/<name>/SKILL.md` is discovered and executed in headless runs. - `supports_new_conversation` now includes the new variant. Exhaustive matches broken by the new variant are filled in so the workspace still builds: - conversation/service.rs: rules ride `preset_context`, same as ACP. - conversation/session_context.rs: rejects with an explicit "not wired yet" error until AgentSessionKind::Antigravity and its factory land, rather than silently falling through to another backend. - ai-agent agent_types_integration test: unreachable arm (that test only exercises Acp and Aionrs).
Models the NDJSON stream that `agy -p --output-format stream-json` emits: `init`, `step_update` and `result` envelopes. Shapes are taken from captured samples under protocols/samples/antigravity-cli/1.1.8, not inferred. Tolerance is deliberate. agy self-updates (1.1.8 -> 1.1.9 was observed while this was being written), so every field is optional or `#[serde(other)]` guarded, an unknown step type degrades to `Unknown` instead of aborting the turn, and `parse_line` returns None for blank lines and for the plain-text notices agy interleaves with the stream. Two shapes are load-bearing and have their own tests: - `init.model` is the authoritative echo of the model actually in use. Its ABSENCE means agy rejected `--model` and silently fell back to its default; a bare or bogus id yields neither the key nor an error on stderr. - a logged-out run fails fast with `status:"ERROR"`, an `error` string and an EMPTY `conversation_id`, which must never be persisted as a resume anchor. `call_mcp_tool` parameters are covered too: agy routes every MCP tool through that single tool and puts the real target in `ServerName` / `ToolName`. The types have no production consumer until the translator lands, hence the module-level dead_code allowance.
Folds the agy NDJSON stream into the backend-neutral event vocabulary the reducer already understands, so no reducer or FSM change is required. Mapping: - init -> BackendBound (the resume anchor) - agent_response -> MessageDelta - tool ACTIVE -> ToolCall; tool DONE/ERROR -> ToolResult - result -> TurnResult, plus UsageDelta when usage is present - user_input / checkpoint / system_message / unknown -> nothing Demux uses agy's `step_index`: it is monotonic within a conversation and keeps counting across a `--conversation` resume, so `step-<index>` reliably pairs a tool's ACTIVE frame with its DONE frame. Three behaviours are pinned by tests because getting them wrong fails silently rather than loudly: - thinking: agy reports `thinking_tokens` but emits no thinking TEXT, so no ThoughtDelta is ever synthesized; doing so would render an empty card. - resume anchor: a failed run reports `conversation_id: ""`. Binding that would launch the next turn with an empty `--conversation`. - model fallback: a missing `init.model` is recorded as a fallback signal, since that absence is exactly how agy reports that it rejected `--model` and quietly used its default instead.
…r turn) Implements BackendConnection / SessionBackend for the agy CLI. The shape differs from the claude lane in one load-bearing way: agy has no `--input-format`, so a TURN IS A PROCESS. `open_session` therefore spawns nothing — it only registers the session and pre-seeds the resume anchor; the first process appears on the first Send and exits when that turn ends. argv: `-p <prompt> --output-format stream-json --dangerously-skip-permissions`, plus `--add-dir <workspace>` and, when set, `--conversation` / `--model` / `--mode`. - `--add-dir` is not optional in practice: without it agy runs tools in its own scratch directory and cannot see any of the conversation's files. - the permission gate is opened deliberately. agy cannot prompt in headless mode and soft-denies every confirmable tool with the gate shut, and a PreToolUse hook returning "allow" cannot override that (hooks tighten, they do not loosen). AionUi's own hook becomes the sole gatekeeper instead. - no `--effort`: agy's model ids already carry the effort suffix, and a stripped id is silently ignored while it falls back to another model. The reader task drains stdout, translates each line, and writes the agy conversation id back to the SESSION's anchor so the next turn can resume. It also clears the process slot on exit, so a later Cancel cannot try to kill an already-exited pid. A run that ends without a `result` frame (cancel, crash) still gets a synthesized terminal, so the FSM never hangs waiting for one. A silently ignored `--model` is surfaced as a warn log — agy signals that condition only by omitting `init.model`, with nothing on stderr. Capabilities describe what agy can actually do: no steering (it ignores stdin mid-turn), no rewind, text-only prompts, synthesized prompt-ack.
Completes the path from a conversation to a running agy process:
AgentSessionKind::Antigravity -> factory::antigravity ->
build_antigravity_instance -> AntigravityConnection -> SessionAgentTask.
build_antigravity_instance sits ALONGSIDE build_session_instance rather than
inside it. That function carries claude/codex-private assembly — cc-switch
provider env, the codex sandbox/approval derivation, persisted-effort replay,
and an `if claude {..} else {"codex"}` label that would silently mislabel any
third backend. The shared helpers (spec/mode/model resolution, the MCP fold,
spawn_env, catalog write-back) are reused; the private parts are not.
`cli_program` stays None on purpose: agy is a large native binary the user
installs themselves, not something the app bundles, so the backend spawns the
`agy` found on PATH.
Migration 034 seeds the builtin catalog row. Two schema details are easy to get
wrong and both fail silently:
- `agent_id` is NOT NULL since 030 and mirrors `id` for builtin rows. Omitting
it fails the constraint, `INSERT OR IGNORE` swallows the failure, and the
migration reports success while seeding nothing at all.
- `behavior_policy` must NOT carry `supports_team`. Migration 033 retired the
`supports_team: false` form (it reads like a denial but is a no-op inside an
OR) and a test enforces its absence. Team capability is derived instead.
McpSupportPolicy is written explicitly rather than falling through to the
all-true default: agy supports stdio and SSE only, and the fallback would let
users configure HTTP-transport MCP servers that write cleanly into agy's config
and then never connect.
Seed-row count assertions move 42 -> 43 for the new builtin.
agy cannot prompt for tool permission in headless mode, and a PreToolUse hook can only TIGHTEN a decision, never loosen one. So the session opens agy's gate with --dangerously-skip-permissions and registers AionUi's own binary as that hook, making AionUi the sole gatekeeper — every decision made at runtime by the user rather than by a policy file. Flow: agy -> hook process (stdin JSON) -> callback endpoint -> the session's permission card -> the user's choice -> hook stdout JSON -> agy. Verified against agy 1.1.8 while building this: - `hooks.json`'s `command` is a COMMAND LINE, not a bare path. agy word-splits it and honours double quotes, so the entry is `"<binary>" antigravity-hook`. Writing just the path would launch the whole backend instead of the hook, and leaving it unquoted would break any install path containing a space. - agy passes its own environment to the hook child process, which is what makes the app -> factory -> spawn_env -> agy -> hook chain work at all. The written `hooks.json` carries no policy, only the registration; a test asserts no literal allow/deny ever reaches the file. Safety properties, each pinned by a test: - fail-closed everywhere. A missing, wrong or foreign token, an absent session, an unparseable request, an unreachable backend and a dropped answer channel all resolve to `deny`. An unapproved tool must never run just because the bridge was unhealthy. - no stranded hooks. Turn end, cancel and teardown drain the pending table as denials. A hook left waiting holds agy's tool call open until --print-timeout (5 minutes), which the user experiences as an unexplained hang. - per-conversation tokens, reissued on every session build, so a stale hook process cannot answer for a rebuilt session or for another conversation. The endpoint is deliberately NOT behind auth_middleware: the hook is a local process with no user session. Since this router applies auth per route group, the handler verifies the token itself. SessionBackend gains `request_external_permission`, defaulting to `Denied`. Only Antigravity implements it — other backends raise permissions on their own wire and are unaffected, and a backend that cannot ask the user never silently allows.
Without this the model picker is empty and every turn silently runs on agy's
default model — the exact condition the earlier `init.model` warning reports.
`agy models` prints one id per line. Those ids already encode reasoning effort
(`-high` / `-medium` / `-low`) and agy only accepts a complete one, so they are
surfaced verbatim and `reasoning_efforts` stays empty. Advertising a separate
effort axis would let the UI assemble a stripped id, which agy drops while
falling back to another model without reporting anything.
Parsing rejects anything that is not a bare token, so the signed-out notice
("Error: Please sign in to view available models…") cannot end up in the picker
as if it were a model.
The probe runs OFF the open path. It costs a process launch, and a connection
is built per conversation, so probing inline would add that latency to every
session open for a list only the picker needs. The catalog write-back already
polls `capabilities()` for late discovery and picks the result up when it
lands; until then the session simply runs on agy's default.
Modes need no probe — agy's `--mode` axis is a fixed default / accept-edits /
plan — so they are reported directly, along with the current model and mode, so
both pickers highlight the active selection.
…ol names Two halves of the same gap: without the config an Antigravity teammate has no team tools at all, and without the name expansion every team step reads as an indistinguishable "call_mcp_tool". Config: agy has no per-run flag for MCP — it only reads files — so the session's servers (team coordination first, then the user's) are written to `<workspace>/.agents/mcp_config.json` before the first turn spawns. Workspace scope is what gives each conversation its own set, and a workspace config is honoured in headless runs (verified). Transport mapping follows what agy actually supports, stdio and SSE: - Stdio -> command / args / env (env becomes an object; we carry ordered pairs) - Sse -> serverUrl / headers - Http -> SKIPPED with a warning. Emitting it as `serverUrl` would produce a config that looks valid and then never connects, since SSE and streamable-HTTP are different protocols. An empty server set removes the file rather than leaving one behind, so a session cannot inherit the previous session's servers. Names: agy routes every MCP tool through the single `call_mcp_tool` tool and puts the real target in the parameters. Both the tool card and the permission card now show `<server>/<tool>`; a malformed call degrades to the raw name instead of producing "/"-junk. Non-MCP tools are untouched. This closes the ordering risk noted in the plan: `team_capable` was already true by derivation, so Antigravity could be picked for a team before anything wrote its MCP config.
Two fixes for the same user-visible failure.
Deep probe: `POST /api/agents/{id}/health-check` routed agy into the
`try_connect_custom_agent` branch, which performs an ACP-style handshake. agy
does not speak ACP, so every health check failed with `acp_init_failed` — an
error about a protocol the user never chose. It now takes the same PATH +
`--version` path as the other direct CLIs.
Turn errors: agy reports a signed-out run as `authentication failed or timed
out`, which says nothing actionable. That case is now rewritten to name the fix
("run `agy` in a terminal to log in") while keeping the original text for
diagnosis. This message matters more than it looks: `agy --version` exits 0
whether or not the user is signed in, so probing CANNOT detect the condition
and this is the only signal they get.
Only the auth case is recognised. Other failures pass through untouched rather
than being given an invented explanation.
The input box no longer locks while a turn runs. agy genuinely cannot take input mid-turn — writing to its stdin during a run is ignored, verified — so there is no steering wire and `supported_commands.steer` stays false. But its next turn is a fresh process regardless, so a message sent now can simply become that next turn. A Send arriving mid-turn is therefore accepted with `Admission::Queued` (an existing state meaning "accepted, waiting for the current turn"), and the reader starts it when the running process exits, resuming the same agy conversation. Details that matter: - queued messages stay SEPARATE and ordered. Merging them would collapse two things the user said into one turn, and the second would never be echoed. - Cancel discards the queue. The user asked to stop; running what they queued afterwards would be the opposite of what they meant. - the reader holds a Weak self-reference: the trait's `dispatch(&self)` cannot thread an Arc through, and a strong one would keep the session alive forever. This is a better outcome than the ACP backends, which set `accepts_proactive_input: false` and leave the input box locked for the whole turn.
agy has no way to list its commands — `agy commands` / `agy skills` both drop into the TUI and headless has no equivalent — but `-p "/<skill-name>"` does invoke a skill (verified). Since AionUi provisions those skills itself under `.agents/skills/`, the command list is read off the skill files instead of asked for. Frontmatter parsing is deliberately minimal (only `name` and `description`) so a skill using richer YAML elsewhere cannot break discovery. The folded `>-` form agy's own builtin skills use is supported. A malformed skill is skipped rather than fatal: one bad file must not cost the user their whole command list. Results are sorted so the picker does not reshuffle between sessions on filesystem ordering.
Model discovery hung forever and the picker stayed silently empty (`available_models: None` in the catalog, no error anywhere). `agy models` prints its list and then keeps running until stdin reaches EOF. `probe_models` bound the stdin handle for the whole function, so the pipe stayed open, stdout never reached EOF, and the read loop never returned — verified against agy 1.1.9: stdin open leaves the process alive indefinitely, stdin closed exits in ~3s. Drop the stdin handle immediately, and add a 30s timeout that kills the child, so a hung agy cannot leak a process from this detached task. The regression test spawns a stand-in that waits on stdin the same way; it times out and fails if the handle is held open again.
The write-back gave up 5s after session open. agy discovers its models by running `agy models` off the open path, which takes ~3s cold and longer on a slow network — close enough to the deadline that the model picker could end up permanently empty for a session, with no error to explain it. Keep polling for 35s for a late model list, while still publishing the modes/commands partial at the original 5s mark so a backend that legitimately reports no models (claude/codex fill capabilities from the handshake) is not held back by the longer window. Adds a test-only catalog channel so this logic can be exercised without standing up a registry.
…as an ACP-family conversation Picking Antigravity in the AionUi UI produced "The selected Agent failed to start" after a 30s timeout, with a working agy installed. The renderer puts every non-aionrs agent on the ACP chat surface, so an agy conversation reaches the ACP factory. agy does not speak ACP: falling through handed it to the AcpAgentManager, which waited for an initialize handshake agy can never answer. Only conversations created with an explicit Antigravity type — which the UI never sends — took the direct-CLI path, so every backend test passed while the product was broken. Resolve the runtime from the backend label instead, and delegate agy to its own factory. The mapping is now a named function with tests, so the "which runtime does this backend need" decision has one home rather than being spread across inline string matches.
…ng the turn Two problems visible from the UI. The catalog row points at `logos/ai-major/antigravity.svg`, which was never added, so every request 404'd and the agent rendered as a broken image in the conversation list and header. The asset is Antigravity's own mark, taken from the google-antigravity GitHub organisation avatar and embedded at the size the UI actually renders it. Separately, a turn could fail outright with "invalid model selection". Model discovery runs off the open path, so before it lands the picker has no agy models to offer and the conversation is seeded with whatever model the user last used elsewhere (e.g. `gemini-3.1-pro-preview`), which agy rejects. Drop a model agy never reported and let it use its own default: running on the default beats failing the conversation over a picker default the user never chose. An empty list means "not discovered yet", not "nothing is valid", so it still passes the request through.
…its deltas Chinese (and any non-ASCII) replies came back with mojibake: "设计和构建" rendered as "设计���构建", both on screen and in the database. agy cuts `text_delta` on byte boundaries, so a multi-byte character that straddles two deltas is decoded as two U+FFFD before it ever reaches us — verified by reading agy's raw stdout directly: `--output-format stream-json` contains U+FFFD where `json` and `text` contain none. The bytes are gone by then, so the deltas cannot be repaired. Its `result` frame carries the same reply assembled once, and intact. Buffer the assistant text and emit it from there instead. A failed or cancelled turn has no authoritative reply, so the buffered deltas are still emitted — agy's own mojibake beats losing the partial answer. Trade-off: the reply now appears when the turn ends rather than growing as deltas land. agy's deltas are coarse anyway (4 frames for a 290-character answer), so this costs little, and a correct answer beats a smoothly streamed corrupted one.
… the task is gone
Switching away from a conversation and back blanked the token usage
indicator: `GET /api/conversations/{id}/usage` answered
`NOT_FOUND: No active agent for this conversation`.
The handler required a live task, so a reaped session had no answer — but
the figure it needs is already durable in
`acp_session.session_config.runtime.context_usage`, and
`SessionAgentTask::get_usage` reads it from exactly there. The live-task
requirement defeated that: usage disappeared precisely when the user
returned to an idle conversation, which is when the indicator matters most.
Fall back to reading the persisted snapshot directly when no task is live.
Reproduced by restarting the backend (equivalent to a reaped task) against
a conversation with recorded usage: NOT_FOUND before, the same figure the
hot task reported after.
This is not Antigravity-specific — every direct-CLI backend lost its usage
the same way.
…ir state needs Antigravity conversations had no `acp_session` row, so everything the session is supposed to remember had nowhere to land: the context-usage snapshot (the indicator read back zero), agy's resume anchor, and the observed mode/model. Row creation was gated on `AgentType::Acp` with the comment "other agent types have no session-level state" — true when it was written, not true for a direct-CLI agent that resumes its own conversations. The seeded `current_mode_id` was gated the same way, leaving agy's mode picker without its initial value even though it has the same three-mode axis. Also caches the discovered model list process-wide. agy's models belong to the signed-in account, not to a conversation, so re-running `agy models` per session only delays the picker — the renderer fetches config options once, so a session that opens before discovery lands shows "use the CLI's model" for its whole life. The first-ever session still races; later ones no longer do. Empty results are never cached, so "not signed in" cannot get pinned. Found by driving the real UI: every earlier check created conversations with an explicit `type` the UI never sends, which produced the ACP row and hid all of this.
11 tasks
added 2 commits
August 1, 2026 23:19
…ps waiting Two problems, both found by measuring agy instead of reasoning about it. **Full auto had no way to be expressed.** `full_auto_mode_id` returned the neutral `default` and `yolo_id` was NULL, so a caller meaning "run unattended" (a teammate via `session_mode_for_backend`, a scheduled run via `fallback_full_auto_mode`) was indistinguishable from a user who simply picked the default mode — and the backend, unable to tell, kept asking for approval nobody was there to give. Sniffing the session's origin instead would need a new branch for every future caller. `yolo` is now a sentinel mode id carried through the channel every other agent already uses, and offered in the mode list so a user can choose full auto deliberately. It is never forwarded as agy's `--mode`: agy accepts unknown mode values silently, so passing it would look fine and quietly fall back. Measured on agy 1.1.9, none of its modes is full auto: default / accept-edits / plan are all refused the "command" permission without `--dangerously-skip-permissions` and all three run commands with it. agy's own wording is `accept-edits: file edits auto-approved` and `plan: research & plan only` — the flag alone decides. **The approval hook was fail-open past ~30s.** The old code deliberately had no deadline, on the assumption that a card may wait indefinitely. It may not: agy abandons a PreToolUse hook after ~30s and runs the tool anyway. Measured with a hook that blocked 180s and never answered, the tool executed at 29.5 / 29.7 / 29.8 / 30.0s across four runs while the card was still on screen — the user believes they have not approved anything, and whatever they click next changes nothing. (agy does honour an actual `deny`; a separate run confirmed the command is refused and the reason is reported back. The problem was only the wait.) Answer `deny` at 20s and retract the card, turning agy's fail-open into fail-closed: a slow user gets a refusal they can retry, never a tool that ran behind a pending prompt. An answer given in time is unaffected. Migration 034 carries the new `yolo_id` and mode list, plus an UPDATE, because `INSERT OR IGNORE` leaves an already-seeded row untouched.
…n 034 The migration now seeds `yolo_id = 'yolo'` and a fourth mode, so the assertions pinning NULL and three modes no longer describe the contract. Both are rewritten to state why the sentinel exists, and a new assertion pins that it is never the seeded `current_mode_id` — starting a fresh conversation with approval prompts off would be a silent downgrade.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds Antigravity (Google's
agyCLI) as a first-class direct-CLI agent. agy does not speak ACP, so it gets its ownAgentTypeand backend rather than being bent onto the ACP path.Requested in iOfficeAI/AionUi#3764, iOfficeAI/AionUi#3454, iOfficeAI/AionUi#3516 and iOfficeAI/AionUi#3240 (the last one specifically as a Gemini CLI successor). iOfficeAI/AionUi#3385 shows the custom-agent workaround failing, which this replaces with a builtin.
Frontend counterpart: iOfficeAI/AionUi (branch
feat/antigravity-agy-support) — the renderer needs a handful of small changes before agy is usable at all.Shape
One process per turn: agy has no
--input-format(its flag set is validated by Go'sflagpackage, so an unknown flag is a hard error), which rules out the persistent-stdin FIFO the claude lane uses. Continuity comes from--conversation <id>, which agy resumes from its own store.Permission is the part that shaped the design. agy cannot prompt in headless mode, and its
PreToolUsehook can only tighten a decision — a hook returningallowcannot override a headless auto-deny. So the gate is opened by--dangerously-skip-permissionsand every real decision comes from a hook that calls back into AionUi. Every failure path on that callback denies.Two measured facts shape this, both of which contradicted an earlier assumption:
default/accept-edits/plan) are refused the"command"permission without the flag and all three run commands with it — the flag alone decides. agy's own descriptions areaccept-edits: file edits auto-approved(edits only) andplan: research & plan only. Full auto is therefore expressed as a sentinel mode id (yolo) travelling the sameyolo_id/full_auto_mode_idchannel every other agent uses, so a teammate or a scheduled run needs no backend special-casing; it is also offered in the mode list so a user can pick it deliberately. The sentinel is never forwarded as agy's--mode— agy accepts unknown mode values silently, so passing it would look fine and quietly fall back.PreToolUsehook after roughly 30 seconds and runs the tool anyway. So the hook answersdenyat 20s and retracts the card, which makes the refusal ours rather than agy's silent approval. Details under "Bugs found".What works
Model discovery (
agy models), the three-mode axis, workspace MCP servers (stdio + SSE; HTTP is skipped rather than mistranslated), skills as slash commands, queued input during a turn, and actionable sign-in errors.Bugs found while verifying, and fixed here
agy modelshung forever. It prints its list and then waits on stdin, so holding the handle open meant stdout never reached EOF and discovery never returned — the model picker stayed silently empty with no error anywhere. Verified against agy 1.1.9: stdin open leaves it running indefinitely, stdin closed exits in ~3s.Antigravity was routed to the ACP manager. The guid page creates a conversation without a type and the backend derives it from the assistant's agent, so an agy conversation reaches the ACP factory. Falling through handed it to
AcpAgentManager, which waited for an initialize handshake agy can never answer — surfacing as "The selected Agent failed to start" after 30s. Only conversations created with an explicit Antigravity type took the direct-CLI path, and the UI never sends one, so every backend test passed while the product was broken.Mojibake in non-ASCII replies. agy cuts
text_deltaon byte boundaries, so a multi-byte character straddling two deltas arrives as two U+FFFD — "设计和构建" rendered as "设计\u{fffd}\u{fffd}构建". Confirmed by reading agy's raw stdout:--output-format stream-jsoncontains U+FFFD wherejsonandtextcontain none. The bytes are gone by then, so the deltas cannot be repaired; itsresultframe carries the same reply intact, and that is now the source. Trade-off: the reply appears when the turn ends rather than growing as deltas land. agy's deltas are coarse anyway (4 frames for a 290-character answer).A foreign model id failed the turn outright. Before discovery lands the picker has no agy models to offer, so the conversation is seeded with whatever model the user last used elsewhere (e.g.
gemini-3.1-pro-preview), which agy rejects with "invalid model selection". A model agy never reported is now dropped so it runs on its own default. An empty list means "not discovered yet", not "nothing is valid".Usage vanished on returning to a conversation.
GET /api/conversations/{id}/usagerequired a live task, so a reaped session answeredNOT_FOUND— the indicator went blank exactly when the user came back, which is when it matters. It now falls back to the durable snapshot inacp_session.session_config.runtime.context_usage. This was not Antigravity-specific: every direct-CLI backend lost its usage the same way.Antigravity conversations had no session row at all. Creation of the
acp_sessionrow was gated onAgentType::Acpwith the comment "other agent types have no session-level state". That left agy with nowhere to persist the context-usage snapshot (the indicator read zero), its resume anchor, or the observed mode/model. The seededcurrent_mode_idwas gated the same way, so the mode picker opened without its initial value. Every earlier check created conversations with an explicittypethe UI never sends, which produced the ACP row and hid all of it.The model list was re-discovered per session. agy's models belong to the signed-in account, not to a conversation, and the renderer fetches config options once — so a session that opened before discovery landed (~3s) showed "use the CLI's model" for its whole life. The list is now cached process-wide; empty results are never cached, so "not signed in" cannot get pinned.
Full auto could not be expressed at all.
full_auto_mode_idreturned the neutraldefaultandyolo_idwas NULL, so "run unattended" was indistinguishable from "the user picked the default mode" — and the backend, unable to tell, kept raising approval prompts nobody was there to answer, stalling teammates and scheduled runs. Deriving it from where the session came from (team? cron?) would need a new branch per future caller; the sentinel needs none.The approval hook was fail-open, which quietly undermined this PR's main claim. The code deliberately had no deadline, assuming a permission card may wait indefinitely. It may not. With a hook that blocked for 180s and never answered, the tool executed at 29.5 / 29.7 / 29.8 / 30.0s across four runs (two tool types, two independent timing methods) while the card was still on screen — the user believes nothing has been approved, and whatever they click next changes nothing. agy does honour a real
deny(a separate run confirmed the command is refused and the reason is reported back to the user), so the defect was purely the wait. Answeringdenyat 20s and retracting the card converts agy's fail-open into fail-closed.One intermediate measurement read -3.7s (artifact before the hook call) and was traced to a flaw in the measuring script, not to agy: the hook overwrote a single timestamp file, so a second invocation erased the first. Re-measured with every invocation appended, it agreed with the rest at 29.7s.
The catalog write-back gave up too early. Its 5s window barely covered a ~3.4s cold discovery. It now watches for 35s while still publishing modes/commands at the original 5s mark, so a backend that legitimately reports no models is not held back.
Known limits
agy's own:
agy commands/agy skillsdrop into the TUI, the binary yields no reliable list, and/helpis answered by the model itself — not a source worth trusting. Workspace skills under.agents/skillsare surfaced; the global~/.agents/skillsis not read by agy (verified with a probe skill).Ours:
Verification
Full gate green. Beyond unit and integration coverage, the flow was driven from the real AionUi UI — select the Antigravity assistant, send a message, get a reply — because both UI-path bugs above were invisible to bridge-created conversations, which carry an explicit type the UI never sends.