DEV Community

Cover image for Why your agents are talking past each other — message schema that fixes it
Alex Aslam
Alex Aslam

Posted on

Why your agents are talking past each other — message schema that fixes it

I once watched two agents have a five-message conversation in which they agreed on everything and accomplished nothing.

The planner sent the executor a task. The executor confirmed it understood. The planner acknowledged the confirmation. The executor reported completion. The planner thanked it and moved on. Every message was polite, coherent, and grammatical. And the executor had done the wrong thing entirely, because "process the recent documents" meant one thing to an agent that had seen the file listing and another thing to an agent that hadn't.

Nobody lied. Nobody hallucinated. The messages were fine. The meaning didn't survive the trip.

That was the week I stopped thinking about agent communication as a language problem and started treating it as a schema problem.

The Drift Is Semantic, Not Syntactic

The failures I kept hitting didn't look like communication failures. They looked like reasoning failures, which is why I spent months tuning the wrong thing.

An agent returns "unable to verify claim X." The next agent summarizes that as "claim X not confirmed." The third agent writes "claim X is disputed." The final report says the source is unreliable. Four hops, four reasonable paraphrases, one original fact, and a conclusion that nobody ever stated. The meaning didn't get corrupted. It got smoothed — each hop replaced a specific status with a more general one, and the generality accumulated.

The research has a name for this now. The "When 'Must' Becomes 'Maybe'" paper studied it in controlled episodes: upstream state gets transformed into intermediate language artifacts — summaries, plans, handoff notes — from which downstream components act. The finding is the one that should be taped above every agent handoff: semantic availability does not guarantee operational preservation. An artifact can mention an unresolved condition while changing its role from something that must be resolved before execution to something that may inform but no longer determines the next action. Across 1,296 controlled episodes, normal handoff compression produced 100% deactivation of binding state and 54.2% forbidden action.

The information was still there. The constraint was gone. And every downstream agent, reading prose, had no way to tell the difference between "verified, fresh, authoritative" and "provisional, stale, secondhand."

We Already Solved This and Forgot

Here's the part that stings. This isn't a new problem.

In 2000, FIPA published the Agent Communication Language specification, and it had exactly the structure I was missing. Every message carried a performative — inform, request, propose, refuse, agree, failure. The performative told the receiver what kind of act the message was, independent of its content. "The documents are stale" as an inform is a different thing than the same sentence as a request to re-fetch. FIPA-ACL made that distinction machine-readable instead of leaving it to the receiver's interpretation.

It also carried conversation identifiers, protocol identifiers, and reply-with fields so that messages could be tracked as coherent exchanges rather than a stream of text.

We threw that away. Not deliberately — it just didn't survive the transition to LLM agents, where the natural thing to do was pass strings to a model and let it figure out the intent. And the model does figure out the intent. Most of the time. The 5% of the time it doesn't is where the interesting bugs live.

The Agent2Agent protocol is a partial rediscovery of this. A2A's core objects are a Task, a Message, and an Artifact — the task has an ID, a context ID, a state, a history, and artifacts. The state is explicit and enumerable: submitted, working, input-required, completed, failed, canceled, rejected. The receiving agent doesn't infer whether the sender is asking or informing. The protocol says.

But A2A defines the envelope. It doesn't define the payload. And most teams, having adopted A2A, still stuff prose into the Message and call it done.

The Schema That Fixed It

I rebuilt my handoffs around a typed message with ten fields. Not all ten are needed for every message. All ten exist because I hit a bug that required each one.

class AgentMessage(BaseModel):
    # WHAT KIND OF ACT IS THIS?
    performative: Literal["inform", "request", "propose", "refuse", "failure"]

    # WHAT IS THE TYPED PAYLOAD?
    artifact_type: str          # e.g. "extracted_claims/v3"
    payload: dict               # validated against the schema above

    # WHERE DID IT COME FROM, AND CAN I TRUST IT?
    provenance: Provenance      # source_uri, retrieved_at, tool_call_id, verified: bool

    # WHAT MUST BE TRUE BEFORE THIS CAN BE ACTED ON?
    preconditions: list[str]    # unresolved conditions that gate execution

    # WHAT WAS THE SENDER ALLOWED TO DO?
    authority: Authority        # scope, issuer, expiry

    # HOW CONFIDENT, AND ON WHAT EVIDENCE?
    confidence: float
    evidence: list[str]         # pointers, not prose

    # WHAT DID THE SENDER DELIBERATELY NOT INCLUDE?
    not_included: list[str]

    # WHAT DOES THE SENDER EXPECT NEXT?
    expected_next: str

    # HOW DO I DEDUPE A RETRY?
    idempotency_key: str
Enter fullscreen mode Exit fullscreen mode

The last field before the closing brace is the one nobody expects, and it's the one that fixed a class of bug I'd been chasing for weeks.

not_included is the explicit negative space. Before I added it, the receiver had no way to distinguish "the sender considered source B and found it irrelevant" from "the sender never looked at source B." Absence in a prose summary reads as nonexistence. Absence in a declared list reads as a decision. Half my coverage bugs came from an agent assuming the upstream work was complete when the upstream agent had simply stopped.

preconditions is the field that fixes the "must becomes maybe" problem. The unresolved condition doesn't live in a sentence that can be paraphrased into a suggestion. It lives in a list, and the receiving harness refuses to execute the next action until the list is empty. The constraint isn't communicated. It's enforced.

performative is the field that stops the agreement-that-accomplishes-nothing. When the executor says "understood," that's an inform, not an agreement. The planner can't mistake acknowledgment for completion because the schema won't let the two look alike.

provenance.verified is the boolean that eliminates the "claim X not confirmed" drift. The receiving agent doesn't infer status from phrasing. It reads a field. A claim is verified or it isn't, and no amount of downstream summarization changes the value.

The Two-Representation Rule

The schema above is the machine-readable half. It's not sufficient on its own.

The structured output protocol pattern that has held up in production requires both human-readable prose and a machine-readable JSON block in every handoff. Pure prose throws away the structure. Pure JSON throws away the reasoning. The receiving agent needs the prose to understand why and the JSON to know what.

In practice, the prose is a short natural-language summary — two to four sentences — that a human reviewer can read in a trace. The JSON is the authoritative payload. When they conflict, the JSON wins, and the conflict is logged as a bug in the generating agent.

This matters more than it sounds. A schema alone produces agents that are technically correct and operationally opaque. A trace full of JSON blocks is unreadable at 2 AM. The prose is what lets a human debug the system, and the JSON is what lets the system debug itself.

What Production Teams Are Running

Microsoft's Agent Framework takes the approach of automatically synchronizing full conversation history across all participants in a handoff workflow, with structured outputs as the primary defensive pattern. The framework's guidance is direct: write agent instructions that return full structured outputs rather than summaries, and rely on the framework's context broadcast so receiving agents get complete history rather than a compressed version.

A2A's Task object is the stateful carrier that survives whatever happens next — the delegating agent going offline, the receiving agent failing partway through, both systems disappearing and reappearing before work completes. The task carries the state, not the prompt. The receiving agent reads the task's current state and continues.

The Zenodo handoff receipt protocol specifies that on session restart, an agent must check for the most recent state file, output a [HANDOFF RECEIVED] block, and absorb context_needed and next_session.first_actions. The handoff isn't a message. It's a receipt the receiver acknowledges and acts on.

AAHP v3 replaces verbose chat history transfer with a structured, compressed handoff state that preserves binding constraints while dropping the noise.

Sentex, a context management middleware for multi-agent pipelines, puts every agent output into a shared sentence graph and retrieves exactly the sentences relevant to the next agent's task — within a token budget you set. The receiving agent doesn't get the full transcript. It gets the typed claims that matter.

The LangChain planner-executor GitHub issue is the clearest illustration of what the schema prevents. The planner calls a tool three times, finds the answer, and writes it into the handoff text. The executor, receiving only text, calls the same tool three more times, walks the same wrong paths, and burns the same latency. The two sessions don't share tool call history, fetched data, or session state. The executor can't tell whether the planner's answer is verified or provisional, fresh or stale. It re-derives everything from prose that was never designed to carry operational state. Add provenance.tool_call_id and provenance.verified to the handoff, and the entire failure mode disappears.

Where the Schema Doesn't Help

I want to be honest about the limits, because I've seen teams over-invest here.

The schema doesn't fix bad reasoning. A perfectly structured message carrying a wrong conclusion is still wrong. The schema makes the reasoning auditable — you can trace a claim back to its source and see where it went sideways — but it doesn't make it correct.

The schema adds latency and tokens. Every field is content the generating agent has to produce and the receiving agent has to parse. On short, simple handoffs, this overhead is real and sometimes not worth it. The handoff-tax research is clear: decomposition becomes preferable only when the attention cost avoided by resetting context exceeds the handoff cost. The same applies to structured handoffs — they're justified when the drift cost is high, not universally.

The schema can't express everything. Some things genuinely are prose. Design rationale, uncertainty about approach, a hunch that something is off but can't be articulated — these resist typing. Forcing them into a field either loses the nuance or produces a field that's always "other". My compromise: typed fields for anything the receiver will act on, free text for anything the receiver will read.

The schema drifts too. Every field is a versioned contract, and contracts decay. I version the artifact types and refuse to resume a handoff whose version doesn't match, with explicit migration paths. That's an ongoing maintenance cost, not a one-time design decision.

The Trade-Off You're Accepting

A message schema buys you semantic preservation and traceability. It costs you velocity and flexibility.

You're writing schemas instead of prompts. You're maintaining version contracts and migration paths. You're accepting that adding a new field is a coordinated change across every agent that produces or consumes the message, not a sentence added to one system prompt. That friction is the point, but it is friction.

And you're accepting that some agents will resist the format. Models produce prose naturally. Getting them to reliably emit a ten-field structured message requires either a constrained decoding setup, a validation-and-repair loop, or a schema-gated tool call. Each of those is infrastructure, and each of them can fail.

But here's what I've learned from every drift bug I've chased: the information was almost always present somewhere in the system. It just wasn't in the message. The sender knew the source was stale. The sender knew the constraint was unresolved. The sender knew which sources it hadn't checked. All of that was in the sender's context, and none of it made it across the boundary, because prose carries what's convenient rather than what's load-bearing.

The teams shipping reliable multi-agent systems aren't the ones with the best models. They're the ones who decided what has to survive the handoff and then built a format that guarantees it.

So here's my question: When your agent hands off work, can the receiver tell the difference between "verified" and "mentioned" — or is that distinction living somewhere in your prose, hoping the next model reads it the same way you meant it?

I'd love to hear where you've landed. A typed message schema, FIPA-style performatives you brought back from the dead, a prose handoff you're still defending — and what finally made you stop tuning the prompt and start designing the message?

Top comments (0)