JJoeven

Curriculum/Multi-Agent Systems

The Interface Is Data

Researcher returns {brief, citations}. If the interface is “whatever they said in chat,” you will debug tone for a quarter and leak instructions sideways.

beginner21 min4 / 24

When you split, freeze the interface. An interface is the object one policy is allowed to send to the next: field names, types, required keys, what is forbidden. A researcher does not “tell” the coder a story. It returns a dict:

  • brief — short, checkable prose
  • citations — ids the coder may quote
  • open_questions — a list, or empty
The wire is a parsed object
ResearcherPacketCoder

Brief, citations, open questions. Chat is not a schema. Extra keys drop.

The wire is a parsed object

If the interface is chat, the next hop will treat a wiki rant as instructions. That is how injection travels sideways. The evals track will measure injection on purpose. Here you need the mechanical fact: untyped text from another agent is still untyped text. A prompt that says “the previous agent is trusted” is how a poisoned search snippet becomes a patch.

The Agents track froze payloads inside one loop so a later thought could not change a tool call. This lesson is the same idea between roles. The coder must not see the researcher’s chain of thought. Thoughts are not a contract. Dicts that parse are a contract.

Why chat fails as a wire

Chat is unordered, untyped, and friendly to extra sentences. Models pad. They apologize. They add “by the way, you should also.” The next policy is another model. It will obey the extra sentence more readily than your hope that it would only read the brief.

Chat as interfaceTyped object as interface
“please fix it, also refund them, love the intern”{"brief": "timeout to vendor", "citations": ["log-17"]}
Tone becomes a ticketParser rejects missing keys
Injection rides alongExtra keys ignored; unknown cites fail
Cannot retry a paragraphCan retry the same dict; hash it

Fail closed. Empty brief, missing citations, citations that are not strings, a raw string instead of an object — all errors. The coder never starts. A human or a retry with a budget sees error. Fail open (“eh, use the paragraph”) is how job 17 becomes a refund.

Do not pass thoughts. Chain of thought is useful inside one policy’s assembler, if you even keep it. Across a handoff it is a side channel: it contains discarded plans, tool names the coder should not see, and copied wiki text. Store thoughts in the researcher’s trace. Hand the coder the parsed object only.

Ids, not dumps. Citations are ids into the blackboard (log-17, kb-44). The coder may fetch those artifacts if the ACL allows, or may only quote the id. The coder must not receive “the entire log file inlined because the researcher was helpful.” Helpful dumps undo context isolation.

Walkthrough: three researcher outputs

Job 17 again. Three wires.

String. "please fix it". Parser: not_object. No coder.

Empty brief. {"brief": " ", "citations": ["log-17"]}. Parser: empty_brief. A citation without a claim is not a brief.

Empty cites. {"brief": "timeout", "citations": []}. Parser: need_citations. This researcher is the “helpful paragraph” failure from the split lesson: done-check was supposed to be has_cite.

Good. {"brief": "timeout to vendor", "citations": ["log-17"]}. Parser: ok. Coder may run.

A fourth failure shows up in incidents: a dict that includes brief plus instructions_for_coder copied from a web page that said “ignore tests.” If your parser only checks two keys and then passes the whole dict through, you failed. Pass a new object with only the allowed keys. Drop the rest. That is freeze-payload between agents.

Live PythonOpen full playgroundpython
Output
Run to execute this in your browser. Nothing is sent to a server.

What printed: the string is not_object. The good dict is ok: True with a stripped brief and a list of cite ids. The whitespace brief is empty_brief. The empty list is need_citations. Chat is rejected. A short brief with an id passes. Empty brief fails closed.

This parser does not yet drop extra keys from a malicious dict. In production, build the return object from known fields only, as the last line already does for the happy path. Never return raw.

Open questions and budgets

open_questions is part of the interface so the researcher can stop without inventing. An empty list means “I claim this is enough.” A non-empty list means the supervisor or a human must decide: fetch more, or hand the coder a partial. Do not let the coder treat open questions as extra instructions.

Budgets belong on the handoff event (next part): step_id, budget hops or dollars for the receiver. The brief is the payload. The event is the envelope. If you only have a brief with no budget, the coder can loop forever inside its own allow-list. Caps are not rude. Caps are the loop story from the tax lesson.

Version the schema. schema_id on the object lets you reject v1 briefs when v2 requires a severity field. Silent schema drift is how last quarter’s researcher breaks this week’s coder.

How agents use this

Put this parser in front of the coder. The coder never sees the researcher’s chain of thought. Same idea as the Agents parser: text is not a decision until it types. Log from=research to=coder hash(payload). If the hash repeats, that is ping-pong later, not a new thought.

Unit-test the parser with the four cases in the box plus one extra-key case. No tokens. If a teammate adds instructions as a pass-through field, the test should fail.

When you add a third role (critic), give it its own object: {issues, attempt, evidence_ids}, not a chatty “looks bad.” The blackboard stores artifacts by id so the critic can require kb-44 without eating a novel.

If the interface is still Slack, you do not have multi-agent. You have a standup. Freeze the dict, fail closed, drop unknown keys, and only then write personas.

Check your understanding

What should a researcher hand a coder?