6.8 KiB
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-jsonit reads one message per line from stdin and runs a turn for each, in one conversation, until stdin closes. A bare-ptakes the next flag as its prompt, so the empty prompt is passed explicitly.--dangerously-skip-permissionssets the permission mode toalways-proceed; without it the mode isrequest-reviewand tool calls wait for approvals print mode can't give.--conversation IDresumes 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 inOptions.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.mdholds the notes (Options.Notes) behindtrigger: always_onfrontmatter. The CLI cuts each rule file at 24,000 bytes, soStartrefuses guidance or notes longer than that.mcp_config.json: one entry per server. Eachmcp.ServerinOptions.MCPServersis a Streamable HTTPurlon the session's listener (below); eachOptions.StdioMCPentry acommandandargs. The CLI namespaces plugin servers as<plugin>_<server>, so byre's own server isbyre_byre.hooks.json: one flat handler per entry inOptions.Hooks, keyed by hook event, each acurlthat 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 thatmcp.Server. A request is answered with its JSON-RPC response asapplication/json; a notification gets202 Accepted. There is no SSE stream.POST /<token>/hook/<event>: runs theOptions.Hooksfunction 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: {...}}:
initarrives at launch, before any input, withconversation_id,model,cwd, the built-intoolsandpermission_mode.Startwaits for it and takes the conversation id from it.step_update: one step of the conversation, bystep_index, withstate(ACTIVEwhile it runs, thenDONE) andstep_type:user_input: a user message, including one a hook injected.agent_response: model output.text_deltastreams its text acrossACTIVEupdates; theDONEupdate carriesduration_secondsandusage(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_nameandtool_info(name,parameters, and onDONEoutput). A shell command isrun_commandwithCommandLine.system_message: a system note, such as the one after a resume.
resultends a turn:status(SUCCESSorERROR),response(the turn's last text),error,duration_seconds,num_turnsandusagetotals 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.