Starphix
Agentic Engineering / Sources
The Lab / Sources / Agentic Engineering / MultiAgent/Communication-Protocols.md

Multi-Agent Communication Protocols

Standardized communication between agents: message formats, protocols, and patterns.


Why Standardized Communication?

Without Standard With Standard
Custom protocols per framework Interoperable agents
Tight coupling Loose coupling
Hard to compose Composable systems
Vendor lock-in Portable agents
Debugging nightmare Observable, traceable

Message Format

Envelope

{
  "protocol": "a2a/1.0",
  "message_id": "uuid-v4",
  "timestamp": "2024-01-15T10:30:00Z",
  "from": "agent://researcher-001",
  "to": "agent://coder-002",
  "reply_to": "agent://orchestrator-001",
  "performative": "request",
  "content_type": "application/json",
  "content": {
    "task": "Implement user authentication",
    "context": {...},
    "constraints": [...]
  },
  "metadata": {
    "priority": "normal",
    "timeout_seconds": 300,
    "requires_ack": true,
    "correlation_id": "workflow-123"
  }
}

Performatives (Speech Acts)

Performative Meaning Response Expected
request Ask agent to perform action Yes (ack + result)
inform Share information/facts No (optional ack)
query Ask question Yes (answer)
propose Offer a plan/deal Yes (accept/reject/counter)
accept Accept proposal No
reject Reject proposal No
counter Counter-proposal Yes (accept/reject)
subscribe Request notifications Yes (confirmation)
notify Push notification No
cancel Cancel previous request Yes (confirmation)

Content Types

Type Schema Use Case
task TaskSchema Delegate work
query QuerySchema Ask question
result ResultSchema Return result
error ErrorSchema Report failure
plan PlanSchema Share plan
context ContextSchema Share context
approval ApprovalSchema Request approval

Transport Protocols

1. HTTP/REST (Request-Response)

# Agent as HTTP server
@app.post("/a2a/message")
async def receive_message(message: A2AMessage):
    # Process message
    result = await agent.handle(message)
    return {"status": "accepted", "message_id": message.message_id}

# Client
async def send_message(to_url: str, message: A2AMessage):
    async with httpx.AsyncClient() as client:
        response = await client.post(f"{to_url}/a2a/message", json=message.model_dump())
        return response.json()

2. WebSocket (Persistent, Bidirectional)

# Server
@app.websocket("/a2a/ws/{agent_id}")
async def websocket_endpoint(websocket: WebSocket, agent_id: str):
    await websocket.accept()
    connection_manager.connect(agent_id, websocket)
    try:
        while True:
            data = await websocket.receive_json()
            message = A2AMessage(**data)
            await handle_message(message)
    except WebSocketDisconnect:
        connection_manager.disconnect(agent_id)

# Client
async def connect_agent(agent_url: str):
    async with websockets.connect(f"{agent_url}/a2a/ws/my-agent") as ws:
        await ws.send_json(message.model_dump())
        response = await ws.recv_json()

3. Message Queue (Async, Durable)

# Using Redis Streams / Kafka / RabbitMQ
class MessageQueueTransport:
    def __init__(self, redis: redis.Redis):
        self.redis = redis
        self.consumer_group = "agents"

    async def send(self, to_agent: str, message: A2AMessage):
        stream = f"a2a:{to_agent}"
        await self.redis.xadd(stream, message.model_dump())

    async def receive(self, agent_id: str, consumer: str) -> AsyncIterator[A2AMessage]:
        stream = f"a2a:{agent_id}"
        while True:
            messages = await self.redis.xreadgroup(
                self.consumer_group, consumer, {stream: ">"}, count=10, block=5000
            )
            for stream_name, messages in messages:
                for msg_id, data in messages:
                    yield A2AMessage(**data)
                    await self.redis.xack(stream, self.consumer_group, msg_id)

4. gRPC (High Performance, Typed)

// a2a.proto
syntax = "proto3";

package a2a;

service AgentCommunication {
  rpc SendMessage(Message) returns (Ack);
  rpc StreamMessages(stream Message) returns (stream Message);
}

message Message {
  string protocol = 1;
  string message_id = 2;
  int64 timestamp = 3;
  string from_agent = 4;
  string to_agent = 5;
  string reply_to = 6;
  string performative = 7;
  string content_type = 8;
  bytes content = 9;
  map<string, string> metadata = 10;
}

message Ack {
  string message_id = 1;
  string status = 2;
}

Agent Discovery

Registry

class AgentRegistry:
    def __init__(self):
        self.agents: dict[str, AgentInfo] = {}

    def register(self, agent: AgentInfo):
        self.agents[agent.agent_id] = agent

    def discover(self, capability: str = None, tags: list[str] = None) -> list[AgentInfo]:
        candidates = self.agents.values()
        if capability:
            candidates = [a for a in candidates if capability in a.capabilities]
        if tags:
            candidates = [a for a in candidates if set(tags).issubset(a.tags)]
        return list(candidates)

    def resolve(self, agent_id: str) -> AgentInfo | None:
        return self.agents.get(agent_id)

@dataclass
class AgentInfo:
    agent_id: str
    name: str
    description: str
    endpoint: str  # HTTP, WebSocket, gRPC
    capabilities: list[str]
    tags: list[str]
    version: str
    metadata: dict

Discovery Protocols

Protocol Mechanism
mDNS/Bonjour Local network discovery
Consul/etcd Service registry
Kubernetes Service discovery via DNS
Custom HTTP /a2a/agents endpoint
DNS-SD DNS-based

Conversation Patterns

1. Request-Response

# Simple delegation
orchestrator -> researcher: request(task="Find papers on transformers")
researcher -> orchestrator: result(papers=[...])

2. Streaming/Progress

# Long-running task with updates
orchestrator -> coder: request(task="Build API", stream=true)
coder -> orchestrator: progress(step="writing tests", percent=25)
coder -> orchestrator: progress(step="implementing", percent=60)
coder -> orchestrator: result(code="...", tests_pass=true)

3. Publish-Subscribe

# Event broadcasting
data_agent -> topic "data.updated": notify(data_id="123", changes={...})
analytics_agent <- topic "data.updated": notify (subscribed)
dashboard_agent <- topic "data.updated": notify (subscribed)

4. Negotiation

# Contract negotiation
buyer -> seller: propose(price=100, quantity=10)
seller -> buyer: counter(price=120, quantity=10)
buyer -> seller: accept

Security

Authentication

# Mutual TLS
ssl_context = ssl.create_default_context()
ssl_context.load_cert_chain("agent.pem", "agent.key")
ssl_context.load_verify_locations("ca.pem")

# Bearer tokens
headers = {"Authorization": "Bearer <agent-token>"}

# Message signing
signed_message = sign(message, private_key)

Authorization

# Capability-based
message.metadata["capabilities"] = ["read:data", "write:reports"]

# Role-based
message.metadata["roles"] = ["analyst", "reporter"]

# Policy engine
if not policy_engine.check(sender, message.performative, message.content):
    raise PermissionError("Not authorized")

Encryption

# End-to-end encryption
encrypted_content = encrypt(message.content, recipient_public_key)
message.content = encrypted_content
message.content_type = "application/encrypted"

# Decrypt on receive
decrypted = decrypt(message.content, private_key)

Observability

Tracing

# Distributed tracing headers
message.metadata["trace_id"] = "abc123"
message.metadata["span_id"] = "def456"
message.metadata["parent_span_id"] = "parent789"

# OpenTelemetry integration
with tracer.start_as_current_span("a2a.send") as span:
    span.set_attribute("a2a.to", to_agent)
    span.set_attribute("a2a.performative", performative)
    await transport.send(message)

Metrics

MESSAGES_SENT = Counter("a2a_messages_sent_total", "Messages sent", ["to", "performative", "status"])
MESSAGE_LATENCY = Histogram("a2a_message_latency_seconds", "Message latency", ["to", "performative"])
MESSAGE_SIZE = Histogram("a2a_message_size_bytes", "Message size", ["performative"])

Frameworks Implementing A2A

Framework Protocol Features
LangGraph Custom Subgraphs, send(), interrupts
AutoGen Custom GroupChat, nested chats
OpenAI Agents SDK Custom Handoffs, function tools
Custom A2A/JSON-RPC Full control


Sources: FIPA ACL, A2A protocol proposals, LangGraph send(), AutoGen GroupChat, OpenAI Agents SDK handoffs, gRPC, JSON-RPC, message queue patterns, distributed systems communication patterns.

From the Agentic Engineering research vault · CC BY 4.0 · all notes · raw markdown