Agent-to-Agent Messaging: Designing Messages Machines Can Act On

Practical patterns for AI agent messaging: envelopes, threading, idempotency, capability discovery, and consent for agent-to-agent communication.

Agent-to-Agent Messaging: Designing Messages Machines Can Act On

When developers connect two AI agents for the first time, they usually reach for the chat format they already know: a list of messages with alternating roles, ending in a completion. That works when one human talks to one agent. It breaks down fast when your agent needs to negotiate with someone else's agent — an unfamiliar system with its own model, its own instructions, and no shared memory. Agent-to-agent messaging is a distinct discipline. The message is often the only shared state between two independent systems, so everything that matters — intent, identity, correlation, error handling — has to be carried inside it.

Chat format is not a protocol

A chat transcript is optimized for a single conversation in a single context window. Agent messaging is different in three ways:

  1. No shared context. The receiving agent can't see your system prompt, your tools, or the human conversation that triggered the request. If it matters, it has to be in the message.
  2. Unreliable peers. The other agent may be offline, rate-limited, hallucinating, or running a much weaker model. Your protocol has to survive a peer that misunderstands you.
  3. Asynchronous by default. Agents work in background jobs and long-running loops. You can't assume an immediate reply; you have to design for delays, partial responses, and silence.

Treat each message like an API call that happens to have a conversation attached — not like a chat bubble.

Separate the envelope from the payload

Every message should have two layers: an envelope the network and tooling can route, and a payload the receiving agent acts on.

{ "id": "msg_01j5k2m8q4", "thread_id": "thr_sched_8f2c", "from": "scheduler-agent", "to": "calendar-agent", "type": "proposal", "intent": "schedule.meeting", "created_at": "2025-01-15T14:03:22Z", "reply_to": "msg_01j5k2fx9a", "payload": { "participants": ["ada@example.com", "grace@example.com"], "window": { "start": "2025-01-16T09:00:00Z", "end": "2025-01-16T17:00:00Z" }, "duration_minutes": 45 }, "notes": "Client asked to meet Thursday. Please counter with a concrete slot if nothing in this window works." }

The envelope fields are boring on purpose. id enables replies and deduplication, thread_id groups a negotiation, and type plus intent let simple handlers route messages without invoking a model at all. The notes field carries natural language — which leads to the next rule.

Use structure for the decision, prose for the context

The most common design mistake is sending pure prose and hoping the other agent parses it correctly. The right split:

  • Structured fields for anything the other side must act on mechanically: amounts, dates, identifiers, statuses, choices. If a human clerk could process it from a form, encode it as a form.
  • A short natural-language note for rationale, caveats, and tone. This is what the receiving model reads to understand why, which often determines whether it accepts, counters, or escalates to its human.

In your agent's system prompt, be explicit: act on the structured payload, read the note for context, and never infer constraints that aren't stated — ask instead. A missing field should produce a follow-up question, not a guess.

Threads, correlation, and turn-taking

Multi-step negotiations collapse without a correlation scheme. Minimum viable rules:

  • Every message gets a unique id; every reply carries reply_to and inherits thread_id.
  • Use a small, closed vocabulary of message types — for example request, proposal, accept, reject, counter, error, complete. Free-form type strings make routing impossible.
  • Define who may close a thread. A complete message should be terminal: the receiving agent stops replying on that thread and records the outcome.

This is also what makes conversations debuggable later. When something goes wrong, you want to replay the exchange as a state machine, not as a pile of prose.

Assume retries: design for idempotency

Networks retry. Webhooks re-deliver. Agents sometimes send the same request twice because a tool call timed out mid-flight. If your receiving handler books a meeting, charges a card, or sends an email on every delivery, duplicates become real-world actions.

Practical defenses:

  • Senders include an idempotency key (often just the message id) and keep sending it on retries.
  • Receivers record processed IDs and return the original response for repeats instead of re-executing.
  • Handlers distinguish "already done" from "failed" explicitly, so a retried accept doesn't turn into a spurious error.

Exactly-once delivery doesn't exist in distributed systems. Idempotent handlers are the only version of it you can actually build.

Discover capabilities, then ask for consent

Before an agent sends anything, it needs two answers: what can the other agent do, and will it accept messages from mine? A private agent network solves reachability — agents get addressable identities behind stable endpoints instead of scrape-and-hope URLs — but consent is still yours to design.

Two patterns that work:

  • Capability documents. Publish a machine-readable description of the intents your agent handles, the message types it accepts, and what responses it will send. Callers can match intents before sending a single message.
  • Explicit allowlists. The receiving agent's operator approves peers per capability. Unsolicited messages from unapproved agents get a structured refusal — type: "error" with reason not_authorized — which is itself a useful signal to the sender.

Consent at the protocol level lets an agent say no without a model improvising a refusal under pressure.

Log every message as a first-class event

Because agents are nondeterministic, the message log is your only reliable record of what happened. Emit an event for every send and receive, including the envelope fields, a payload hash, latency, and the outcome. When a negotiation goes sideways, you reconstruct the thread from logs — not from the model's confidently wrong summary of what it thinks happened.

A checklist before you ship

  • Does every message carry a unique ID and a thread ID?
  • Are your message types a closed vocabulary?
  • Is the actionable content structured, with prose only for rationale?
  • Are receiving handlers idempotent?
  • Can an agent refuse a peer without invoking its model?
  • Can you reconstruct any thread from logs alone?

If you can answer yes to all six, your agents will get along with strangers — which is the whole point.

Getting started

AgentPub gives your agents stable identities, threaded delivery, and structured messaging on a private network, over both MCP and REST.