Message Protocol
Every interaction between an agent and its environment — tool calls, memory operations, skill execution, multi-agent delegation — travels over a single, typed, versioned wire format. This NDJSON protocol is the contract that decouples agents from backends: as long as both sides speak A2E messages, the implementations can evolve independently.
Wire Format
A2E uses NDJSON (newline-delimited JSON) as its wire format. Each line is a self-contained JSON object representing one message. Compact JSON encoding (no extra whitespace) is used for efficiency.
{"a2e":"1.0","id":"a1b2c3","type":"tool/call/req","ts":1716123456.789,...}\n
{"a2e":"1.0","id":"d4e5f6","type":"tool/call/resp","ts":1716123457.012,...}\nA2EMessage Base
Every A2E message inherits from A2EMessage (Pydantic BaseModel):
| Field | Type | Description |
|---|---|---|
type | str | Message type identifier (e.g. "tool/call/req") |
id | str | UUID hex, auto-generated per message |
version | str | Protocol version, always "1.0" |
ts | float | Unix epoch timestamp |
Methods: to_dict() (serialize), new_id() (regenerate UUID).
Protocol Versioning
Protocol version is "1.0", described as a strict superset of SCP 1.0 (Skill Call Protocol). Unknown message types degrade gracefully — the base type map covers SCP-compatible handshake/ping/pong/shutdown/error, and any unrecognized type falls back to a generic A2EMessage.
Message Lifecycle
Correlation
RPC requests are correlated by req_id — the client generates a UUID, the server copies it into the response. The client maintains a dict of req_id -> queue.Queue, so multiple concurrent RPCs can be in flight. Events reference their parent request via req_id.
Event Streaming
A2EEvent (type: invoke/event) provides streaming mid-operation updates:
| Field | Type | Description |
|---|---|---|
kind | EventKind | progress, artifact, log, or status |
req_id | str | Correlates to the originating request |
data | dict | Payload (progress %, artifact content, log text, status message) |
seq | int | Monotonic sequence number within the request |
Events are delivered before the final response, enabling progressive UI updates.
Capability Data Lifecycles
Most message types are RPC request/response pairs correlated by req_id. Some capabilities additionally expose an orthogonal data lifecycle — a set of messages that move task data but never carry the bytes themselves. The canonical example is the env data plane (env/data/{reset,add,get}):
- Requests carry metadata only — paths, checksums, sizes, content types.
- Bytes are moved by the host on its own filesystem (bind-mounted paths, archive extraction), so multi-GB corpora never traverse the protocol.
- The data lifecycle is independent of the operation lifecycle it belongs to: an episode can be reset without touching data, and the data root restored without starting an episode.
This keeps the wire contract small and stable while letting hosts stage arbitrarily large inputs and collect arbitrarily large agent artifacts.
Error Handling
A2EError is returned when a message cannot be processed:
| Field | Type | Description |
|---|---|---|
req_id | str | The failed request's ID |
code | A2EErrorCode | Machine-readable error code |
message | str | Human-readable error description |
detail | dict | Additional structured context |
retryable | bool | Whether the client should retry |
capability_name | str | Which capability namespace failed |
See Error Codes for the full list.
Type Registry
Both client and server maintain a type_registry mapping type strings to Pydantic model classes. The base map (A2E_BASE_TYPE_MAP) covers core types:
| Type String | Model Class |
|---|---|
handshake/req | HandshakeRequest |
handshake/resp | HandshakeResponse |
invoke/event | A2EEvent |
ping | Ping |
pong | Pong |
shutdown | Shutdown |
error | A2EError |
Plugins extend this at startup by registering their own TYPE_MAP entries (e.g. tool/call/req -> ToolCallRequest).