Files
byre/docs/antigravity-protocol.md
T

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-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.