Protobuf Deep Analysis — Antigravity Wire Protocol
Overview
The Antigravity SDK communicates with the localharness Go binary via Protocol Buffers serialized as JSON over a WebSocket connection. The localharness_pb2.py file (430 lines) is auto-generated from localharness.proto and defines the complete wire protocol.
This page documents every message type, every field, every oneof pattern, and the serialization rules.
Wire Format
stdin/stdout (Handshake Phase)
Length-prefixed binary protobuf:
[4 bytes uint32 little-endian: message_length][protobuf bytes: message_body]
This is standard binary protobuf, NOT JSON. The handshake is synchronous and blocking.
WebSocket (Runtime Phase)
JSON-serialized protobuf. The SDK uses google.protobuf.json_format.MessageToJson() and Parse() for serialization. Example wire message:
{
"stepUpdate": {
"cascadeId": "abc-123",
"trajectoryId": "traj-0",
"stepIndex": 3,
"state": "ACTIVE",
"source": "MODEL",
"target": "USER",
"textDelta": "Here are the files",
"listDirectory": {
"directoryPath": "/workspace",
"results": [
{"name": "src", "isDirectory": true},
{"name": "README.md", "isDirectory": false, "fileSize": 1234}
]
}
},
"seqNum": 42,
"timestampMicros": 1721654400000000
}
Field naming: Protobuf field names use snake_case in the .proto definition but are serialized as camelCase in JSON (protobuf JSON mapping convention).
Message Category Map
graph TB
subgraph "Handshake (stdin/stdout)"
IC["InputConfig<br/>SDK → Harness"]
OCI["OutputConfig<br/>Harness → SDK"]
CI["ClientInfo<br/>part of InputConfig"]
end
subgraph "Initialization (WebSocket)"
ICE["InitializeConversationEvent<br/>SDK → Harness"]
HC["HarnessConfig<br/>central config message"]
end
subgraph "Model Configuration (oneof in HarnessConfig)"
GC["GeminiConfig"]
GMC["GemmaConfig"]
CBC["CustomBackendConfig"]
end
subgraph "Runtime: Harness → SDK"
OE["OutputEvent<br/>main envelope"]
SU["StepUpdate<br/>workhorse message"]
TSU["TrajectoryStateUpdate"]
TC["ToolCall"]
UM["UsageMetadata"]
end
subgraph "Runtime: SDK → Harness"
IE["InputEvent<br/>main envelope"]
UI["UserInput"]
TCF["ToolConfirmation"]
TR["ToolResponse"]
UQR["UserQuestionsResponse"]
end
subgraph "Configuration Sub-messages"
SI["SystemInstructions"]
TL["Tool"]
HST["HarnessSideTools"]
WS["Workspace"]
MCP["McpServerConfig"]
end
OE --> SU
OE --> TSU
OE --> TC
OE --> UM
ICE --> HC
HC --> GC
HC --> GMC
HC --> CBC
HC --> SI
HC --> TL
HC --> HST
HC --> WS
HC --> MCP
Handshake Messages
InputConfig (SDK to Harness, stdin)
Sent as the first message over stdin. The harness blocks until it receives this.
| Field | Proto # | Type | Default | Description |
|---|
storage_directory | 1 | string | "" | Path for trajectory persistence on disk |
port | 2 | uint32 | 0 | Requested WebSocket port. 0 = auto-assign |
bind_address | 3 | string | "localhost" | WebSocket bind address |
client_info | 4 | ClientInfo | — | SDK metadata |
Behavior: If port is 0, the harness selects an available port. The actual port is reported back in OutputConfig.port.
ClientInfo (sub-message of InputConfig)
| Field | Proto # | Type | Description |
|---|
language | 1 | string | Always "python" for the Python SDK |
version | 2 | string | SDK version from package metadata (e.g., "0.1.2") |
language_version | 3 | string | Python version string (e.g., "3.12.4") |
OutputConfig (Harness to SDK, stdout)
Sent by the harness after it has started its WebSocket server.
| Field | Proto # | Type | Description |
|---|
port | 1 | int32 | Actual WebSocket port the server is listening on |
api_key | 2 | string | Ephemeral API key for WebSocket authentication header |
The api_key is a randomly generated string, valid only for this harness process lifetime. It is used in the x-goog-api-key header during the WebSocket handshake.
Initialization Messages
InitializeConversationEvent (SDK to Harness, first WebSocket message)
| Field | Proto # | Type | Description |
|---|
config | 1 | HarnessConfig | Complete agent configuration |
This is the ONLY message the SDK sends before the runtime loop begins. The harness uses it to set up the entire agent session.
HarnessConfig (the central configuration message)
This is the most important message in the protocol. It defines the entire agent session.
| Field | Proto # | Type | Oneof Group | Description |
|---|
cascade_id | 1 | string | — | Conversation/session identifier. Used to correlate trajectories |
gemini_config | 2 | GeminiConfig | model_config | Cloud Gemini API configuration |
gemma_config | 3 | GemmaConfig | model_config | Local Gemma model configuration |
custom_backend | 13 | CustomBackendConfig | model_config | External backend (DeepSeek, etc.) |
system_instructions | 4 | SystemInstructions | — | Agent persona and instructions |
tools | 5 | repeated Tool | — | Custom Python tool definitions (schemas only) |
harness_side_tools | 6 | HarnessSideTools | — | Built-in tool enable/disable toggles |
compaction_threshold | 7 | uint32 | — | Token count that triggers context compaction |
workspaces | 8 | repeated Workspace | — | Allowed filesystem directories |
skills_paths | 9 | repeated string | — | Directories to scan for skill files |
finish_tool_schema_json | 10 | string | — | JSON schema for structured output via FINISH tool |
initial_trajectory | 11 | bytes | — | Serialized trajectory for session resumption |
app_data_dir | 12 | string | — | Override for harness app data directory |
mcp_servers | 14 | repeated McpServerConfig | — | MCP server configurations |
model_config Oneof Pattern
The model_config field is a oneof that selects the backend. Exactly one must be set:
oneof model_config {
GeminiConfig gemini_config = 2;
GemmaConfig gemma_config = 3;
CustomBackendConfig custom_backend = 13;
}
| Variant | Use Case | Proto Field |
|---|
GeminiConfig | Cloud Gemini API | 2 |
GemmaConfig | Local Gemma via OpenAI-compatible endpoint | 3 |
CustomBackendConfig | Any external backend (DeepSeek, etc.) | 13 |
If none is set, the harness falls back to environment variable GEMINI_API_KEY and default Gemini config.
Model Configuration Messages
GeminiConfig
| Field | Proto # | Type | Default | Description |
|---|
api_key | 1 | string | — | Gemini API key. Falls back to $GEMINI_API_KEY env var |
base_url | 2 | string | — | Custom API endpoint (overrides default Gemini endpoint) |
model_name | 3 | string | "gemini-3.5-flash" | Model identifier |
thinking_level | 4 | string | — | Extended thinking level (minimal/low/medium/high) |
enable_url_context | 5 | bool | false | Enable URL context grounding |
enable_google_search | 6 | bool | false | Enable Google Search grounding |
use_vertex | 7 | bool | false | Route through Vertex AI |
project | 8 | string | — | GCP project ID (required if use_vertex=true) |
location | 9 | string | — | GCP region (required if use_vertex=true) |
GemmaConfig
| Field | Proto # | Type | Description |
|---|
base_url | 1 | string | OpenAI-compatible endpoint (e.g., "http://localhost:8080/v1") |
model_name | 2 | string | Model identifier (e.g., "gemma-4-12B-it-qat-4bit") |
This is the dedicated config for local Gemma models. The harness sends requests using the OpenAI Chat Completions format to the specified endpoint. See sdk-test-integrations for Gemma 4 MTP pipeline details.
CustomBackendConfig
| Field | Proto # | Type | Description |
|---|
backend_type | 1 | string | Backend identifier (e.g., "deepseek", "ollama") |
config_json | 2 | string | JSON-encoded backend-specific configuration |
This is the escape hatch for any backend not natively supported. The config_json field carries arbitrary configuration that the harness forwards to the backend adapter.
System Instructions Messages
SystemInstructions (oneof)
oneof system_instructions {
CustomSystemInstructions custom = 1;
AppendedSystemInstructions appended = 2;
}
CustomSystemInstructions
Full replacement of the system prompt. Use with caution -- this replaces ALL default instructions.
| Field | Proto # | Type | Description |
|---|
parts | 1 | repeated Part | Content parts (text only for system instructions) |
AppendedSystemInstructions
Appends to the default system prompt. This is the recommended approach.
| Field | Proto # | Type | Description |
|---|
custom_identity | 1 | string | Custom identity statement (e.g., "You are a security analyst") |
sections | 2 | repeated Section | Additional instruction sections |
Section
| Field | Proto # | Type | Description |
|---|
title | 1 | string | Section heading |
content | 2 | string | Section body |
Tool Definition Messages
Tool (custom Python tool schema)
| Field | Proto # | Type | Description |
|---|
name | 1 | string | Function name (e.g., "get_weather") |
description | 2 | string | Function docstring |
parameters_json_schema | 3 | string | JSON Schema for input parameters |
response_json_schema | 4 | string | JSON Schema for response format |
These are schema-only definitions. The harness calls back to the SDK via ToolCall when it needs to execute a Python tool.
HarnessSideTools (11 toggle configs)
Each sub-message has a single enabled bool field:
| Sub-message | Proto # | Tool Controlled | Extra Fields |
|---|
FindToolConfig | 1 | find_file | — |
RunCommandToolConfig | 2 | run_command | — |
SubagentsConfig | 3 | start_subagent | — |
UserQuestionsConfig | 4 | ask_question | — |
FileEditToolConfig | 5 | edit_file | — |
ViewFileToolConfig | 6 | view_file | — |
WriteToFileToolConfig | 7 | create_file | — |
GrepSearchToolConfig | 8 | search_directory | — |
ListDirToolConfig | 9 | list_directory | — |
GenerateImageToolConfig | 10 | generate_image | model_name (string, default: "gemini-3.1-flash-image-preview") |
PermissionsConfig | 11 | workspace validation | enforce_workspace_validation (bool) |
Workspace
| Field | Proto # | Type | Description |
|---|
filesystem_workspace | 1 | oneof | Currently only FilesystemWorkspace variant |
directory | 1 (inner) | string | Absolute path to allowed directory |
Runtime Messages: Harness to SDK
OutputEvent (the main event envelope)
Every message from the harness to the SDK is wrapped in an OutputEvent.
| Field | Proto # | Type | Oneof Group | Description |
|---|
seq_num | 1 | int64 | — | Monotonically increasing sequence number |
timestamp_micros | 2 | int64 | — | Timestamp in microseconds since epoch |
step_update | 10 | StepUpdate | event | Agent step update |
trajectory_state_update | 11 | TrajectoryStateUpdate | event | Trajectory lifecycle state change |
tool_call | 12 | ToolCall | event | Host-side tool invocation request |
usage_metadata | 20 | UsageMetadata | — | Token usage (may accompany any event) |
The event oneof ensures each OutputEvent carries exactly one type of event.
StepUpdate (the workhorse message)
The most complex message in the protocol. At ~1.7KB in the proto definition, it covers every possible agent action.
| Field | Proto # | Type | Description |
|---|
cascade_id | 1 | string | Parent trajectory (conversation) ID |
trajectory_id | 2 | string | This trajectory's ID (differs for subagents) |
step_index | 3 | uint32 | Step number within the trajectory |
state | 4 | State enum | UNSPECIFIED/ACTIVE/DONE/WAITING_FOR_USER/ERROR |
source | 5 | Source enum | UNSPECIFIED/SYSTEM/USER/MODEL |
target | 6 | Target enum | UNSPECIFIED/USER/MODEL/ENVIRONMENT |
error_message | 7 | string | Error details (when state=ERROR) |
thinking | 8 | string | Full thinking text (non-streaming) |
text_delta | 9 | string | Incremental text token (streaming) |
thinking_delta | 10 | string | Incremental thinking token (streaming) |
text | 11 | string | Full accumulated text (non-streaming) |
request_text | 12 | string | User-facing request text |
tool_confirmation_request | 13 | ToolConfirmationRequest | Approval needed for tool execution |
questions_request | 14 | UserQuestionsRequest | Questions for the user |
State Enum
| Value | Name | Meaning |
|---|
| 0 | UNSPECIFIED | Default/unset |
| 1 | ACTIVE | Step is currently executing |
| 2 | DONE | Step completed successfully |
| 3 | WAITING_FOR_USER | Waiting for user input (confirmation or question) |
| 4 | ERROR | Step failed |
Source Enum
| Value | Name | Meaning |
|---|
| 0 | UNSPECIFIED | Default |
| 1 | SYSTEM | System-generated step |
| 2 | USER | User-initiated step |
| 3 | MODEL | Model-generated step |
Target Enum
| Value | Name | Meaning |
|---|
| 0 | UNSPECIFIED | Default |
| 1 | USER | Directed at the user |
| 2 | MODEL | Directed at the model |
| 3 | ENVIRONMENT | Directed at the environment (tools) |
Action Sub-messages (oneof)
Each StepUpdate carries exactly ONE action sub-message:
| Action | Proto Field | Message Type | Key Fields |
|---|
list_directory | 20 | ActionListDirectory | directory_path, results[] (name, is_directory, file_size) |
find_file | 21 | ActionFindFile | directory_path, query, output |
search_directory | 22 | ActionSearchDirectory | directory_path, query, num_results |
view_file | 23 | ActionViewFile | file_path, start_line, end_line |
create_file | 24 | ActionCreateFile | file_path, contents |
edit_file | 25 | ActionEditFile | file_path, diff_blocks[] |
run_command | 26 | ActionRunCommand | command_line, working_dir, exit_code, combined_output |
compaction | 27 | ActionCompaction | (empty) |
invoke_subagent | 28 | ActionInvokeSubagent | (empty) |
generate_image | 29 | ActionGenerateImage | prompt, image_paths[], image_name |
finish | 30 | ActionFinish | output_string (contains structured output JSON if schema set) |
error | 31 | ActionError | error_message, http_code |
mcp_tool | 32 | ActionMcpTool | server_name, tool_name, arguments_json |
ActionEditFile Detail
The edit_file action carries a diff structure:
message ActionEditFile {
string file_path = 1;
repeated DiffBlock diff_blocks = 2;
}
message DiffBlock {
uint32 start_line = 1;
uint32 end_line = 2;
repeated DiffLine lines = 3;
}
message DiffLine {
string text = 1;
LineAction action = 2; // KEEP, INSERT, DELETE
}
ActionRunCommand Detail
message ActionRunCommand {
string command_line = 1;
string working_dir = 2;
int32 exit_code = 3;
string combined_output = 4; // stdout + stderr merged
}
TrajectoryStateUpdate
| Field | Proto # | Type | Description |
|---|
trajectory_id | 1 | string | Trajectory identifier |
state | 2 | TrajectoryState enum | UNSPECIFIED/RUNNING/IDLE |
error | 3 | string | Error message if trajectory failed |
The SDK uses these to determine idle state. A connection is idle only when ALL trajectories (parent + subagents) are in IDLE state.
ToolCall (host-side tool invocation)
Sent when the harness needs the SDK to execute a Python tool.
| Field | Proto # | Type | Description |
|---|
id | 1 | string | Unique call identifier (correlates with ToolResponse) |
name | 2 | string | Tool function name |
args | 3 | map<string, string> | Serialized arguments |
server_name | 4 | string | MCP server name (if MCP tool) |
UsageMetadata
| Field | Proto # | Type | Description |
|---|
prompt_token_count | 1 | uint64 | Input tokens consumed |
cached_content_token_count | 2 | uint64 | Cached subset of input tokens |
candidates_token_count | 3 | uint64 | Output tokens (excluding thinking) |
thoughts_token_count | 4 | uint64 | Thinking/reasoning tokens |
total_token_count | 5 | uint64 | Sum of all token categories |
Runtime Messages: SDK to Harness
InputEvent (the main input envelope)
Every message from the SDK to the harness is an InputEvent with a oneof payload:
| Variant | Proto # | Type | Description |
|---|
user_input | 1 | string | Simple text prompt |
complex_user_input | 2 | UserInput | Multimodal input (text + media) |
tool_confirmation | 3 | ToolConfirmation | Approve/deny a tool call |
tool_response | 4 | ToolResponse | Result of a Python tool execution |
question_response | 5 | UserQuestionsResponse | Answer to agent questions |
halt_request | 6 | bool | Cancel current model turn |
automated_trigger | 7 | string | Trigger notification message |
UserInput (multimodal input)
| Field | Proto # | Type | Description |
|---|
parts | 1 | repeated Part | Content parts |
Part (oneof content type)
message Part {
oneof content {
string text = 1;
Media media = 2;
SlashCommand slash_command = 3;
}
}
Media
| Field | Proto # | Type | Description |
|---|
mime_type | 1 | string | MIME type (e.g., "image/png") |
description | 2 | string | Optional description |
data | 3 | bytes | Raw media data |
SlashCommand
| Field | Proto # | Type | Description |
|---|
name | 1 | string | Slash command name (without leading /) |
ToolConfirmation
| Field | Proto # | Type | Description |
|---|
trajectory_id | 1 | string | Target trajectory |
step_index | 2 | uint32 | Target step |
accepted | 3 | bool | true = approve, false = deny |
ToolResponse
| Field | Proto # | Type | Description |
|---|
id | 1 | string | Correlates with ToolCall.id |
response_json | 2 | string | JSON-serialized result |
supplemental_media | 3 | repeated Media | Attachments |
response | 4 | Struct | Structured result (protobuf Struct) |
UserQuestionsResponse
| Field | Proto # | Type | Description |
|---|
trajectory_id | 1 | string | Target trajectory |
step_index | 2 | uint32 | Target step |
cancelled | 3 | bool | User cancelled (oneof) |
response | 4 | QuestionsResponse | User's answers (oneof) |
MCP Configuration Messages
McpServerConfig
| Field | Proto # | Type | Oneof Group | Description |
|---|
name | 1 | string | — | Server identifier |
stdio | 2 | McpStdioTransport | transport | stdio-based transport |
http | 3 | McpHttpTransport | transport | HTTP-based transport |
enabled_tools | 4 | repeated string | — | Tool allowlist |
disabled_tools | 5 | repeated string | — | Tool denylist |
auth_provider_type | 6 | AuthProviderType enum | — | UNSPECIFIED/GOOGLE_CREDENTIALS |
McpStdioTransport
| Field | Proto # | Type | Description |
|---|
command | 1 | string | Executable path |
args | 2 | repeated string | Command arguments |
env | 3 | map<string, string> | Environment variables |
McpHttpTransport
| Field | Proto # | Type | Description |
|---|
url | 1 | string | HTTP endpoint URL |
headers | 2 | map<string, string> | Request headers |
AuthProviderType Enum
| Value | Name | Description |
|---|
| 0 | UNSPECIFIED | No auth |
| 1 | GOOGLE_CREDENTIALS | Use Google OAuth2 credentials |
Serialization Rules
JSON Mapping
Protobuf JSON mapping rules apply:
- Field names:
snake_case in proto → camelCase in JSON
- Enum values: Sent as string names (e.g., "ACTIVE" not 1)
- Bytes fields: Base64-encoded
- Default values: Omitted from wire format
- oneof: Only the set variant appears
Content Type
All WebSocket messages use the default WebSocket text frame with JSON content. Binary frames are not used.
Message Size
No explicit size limit is enforced by the protocol. Practical limits:
- Individual messages: Typically < 1MB
initial_trajectory: Can be several MB (serialized conversation history)
ActionCreateFile.contents: Can be large for file creation
combined_output in ActionRunCommand: Can be large for command output
Cross-References
Source: localharness_pb2.py (430 lines), auto-generated from localharness.proto. Python SDK: local_connection.py (1792 lines).