Files

69 lines
6.8 KiB
Markdown
Raw Permalink Normal View History

# Antigravity CLI protocol
How `internal/antigravity` drives the Antigravity CLI, `agy` (`brew install --cask antigravity-cli`; `agy --version` prints the version, `agy changelog` the release notes, and `agy --help` the flags). The CLI is signed in once, interactively, by running `agy` with no arguments as the user byre runs as; `agy models` lists the models that login offers, and fails with "Please sign in" until then. The CLI updates itself.
## Launch
```
agy --input-format stream-json --output-format stream-json \
--dangerously-skip-permissions --add-dir <session dir> \
[--model M] [--conversation ID] --print ""
```
- `--print ""` puts the CLI in print mode; with `--input-format stream-json` it reads one message per line from stdin and runs a turn for each, in one conversation, until stdin closes. A bare `-p` takes the next flag as its prompt, so the empty prompt is passed explicitly.
- `--dangerously-skip-permissions` sets the permission mode to `always-proceed`; without it the mode is `request-review` and tool calls wait for approvals print mode can't give.
- `--conversation ID` resumes a conversation with its history; the model comes from `--model`, not from the conversation.
- Runs in the session's working directory, with `GIT_AUTHOR_*`/`GIT_COMMITTER_*` set to the commit identity and anything in `Options.Env`. Commands the agent runs (`run_command`) inherit the environment.
## The session directory
There is no flag for a system prompt or MCP servers, so byre hands them over as a plugin. Each session has its own directory (`Options.Dir`, `antigravity/<session-id>` under the data directory), passed as `--add-dir`. The CLI treats it as a second workspace and loads the plugin in its `.agents/plugins/byre/`:
- `plugin.json`: `{"name": "byre"}`.
- `rules/AGENTS.md`: byre's guidance (`Options.Guidance`), always on. `rules/notes.md` holds the notes (`Options.Notes`) behind `trigger: always_on` frontmatter. The CLI cuts each rule file at 24,000 bytes, so `Start` refuses guidance or notes longer than that.
- `mcp_config.json`: one entry per server. Each `mcp.Server` in `Options.MCPServers` is a Streamable HTTP `url` on the session's listener (below); each `Options.StdioMCP` entry a `command` and `args`. The CLI namespaces plugin servers as `<plugin>_<server>`, so byre's own server is `byre_byre`.
- `hooks.json`: one flat handler per entry in `Options.Hooks`, keyed by hook event, each a `curl` that posts the hook's stdin to the session's listener and prints the reply. Only flat events (`SessionStart`, `PreInvocation`, `PostInvocation`, `Stop`) fit this shape.
Attached images are written under `images/` in the same directory. The directory is removed when the session ends.
## The session's listener
`Start` listens on `127.0.0.1` on a port the OS picks. Every path starts with a random token made per session (`crypto/rand.Text`), written only into the plugin's `mcp_config.json` and `hooks.json`, so a page in a browser or another local process that can reach the port can't call byre's tools without reading those files; any other path is a 404. It serves:
- `POST /<token>/mcp/<server>`: Streamable HTTP MCP for that `mcp.Server`. A request is answered with its JSON-RPC response as `application/json`; a notification gets `202 Accepted`. There is no SSE stream.
- `POST /<token>/hook/<event>`: runs the `Options.Hooks` function for the event on the posted hook input and answers its output as JSON. A failed hook is logged and answers `{}`.
The CLI loads MCP tool schemas lazily. The model reads a tool's schema from `~/.gemini/antigravity-cli/mcp/<server>/<tool>.json` with `view_file`, then calls it through the built-in `call_mcp_tool` tool with `ServerName`, `ToolName` and `Arguments`.
## Messages
User turn, written to stdin: `{"event":"user","message":{"role":"user","content":[{"type":"text","text":"..."}]}}`. `content` may also be a plain string. Only `text` blocks are accepted: an `image` block fails the line with `stream input content block type "image" is not supported`, so byre saves each image to a file and adds a text block naming its path. With `Options.Reminder` set, every message starts with a text block holding it inside `<system-reminder>` tags. A line without `event` or with `event` other than `user` fails or is ignored; `control_request` is "not supported yet", so there is no control channel.
Stdout, one JSON object per line, each `{"event": E, E: {...}}`:
- `init` arrives at launch, before any input, with `conversation_id`, `model`, `cwd`, the built-in `tools` and `permission_mode`. `Start` waits for it and takes the conversation id from it.
- `step_update`: one step of the conversation, by `step_index`, with `state` (`ACTIVE` while it runs, then `DONE`) and `step_type`:
- `user_input`: a user message, including one a hook injected.
- `agent_response`: model output. `text_delta` streams its text across `ACTIVE` updates; the `DONE` update carries `duration_seconds` and `usage` (`input_tokens`, `output_tokens`, `thinking_tokens`, `cache_read_tokens`, `total_tokens`). A response that only calls tools has no text. Thinking is counted but never streamed.
- `tool`: a tool call, `tool_name` and `tool_info` (`name`, `parameters`, and on `DONE` `output`). A shell command is `run_command` with `CommandLine`.
- `system_message`: a system note, such as the one after a resume.
- `result` ends a turn: `status` (`SUCCESS` or `ERROR`), `response` (the turn's last text), `error`, `duration_seconds`, `num_turns` and `usage` totals for the whole conversation, earlier processes included.
Lines that aren't a JSON object, don't parse, or carry an `event` outside `init`, `step_update` and `result` go to `Options.OnBadLine`; lines that parse are still delivered as events.
## Interrupts and model switches
SIGINT ends the running turn: the CLI prints `error: interrupted` on stderr, sends a `result` with `status` `ERROR` and `error` `interrupted`, and exits. A tool step cut off this way gets no `DONE` update. `Session.Interrupt` sends SIGINT and starts a new process on the same conversation (`--conversation`) once the old one exits, so the session stays usable.
There is no way to switch the model of a running process, so `Session.SetModel` closes the idle process's stdin, waits for it to exit, and starts a new one with the new `--model` on the same conversation.
A process that exits when byre didn't ask it to ends the session: `Events` closes, and the listener and session directory go away.
## Hooks
Hook input and output are camelCase JSON. byre uses `PreInvocation`, which runs before every model call with `invocationNum` (0 for a turn's first call) and `initialNumSteps`; an output of `{"injectSteps":[{"userMessage":"..."}]}` adds a user message the model reads on that call. The `Stop` hook gets no final message, so byre doesn't use it.
## Forking
The CLI can't fork a conversation at a given point, so Antigravity sessions can't be forked.