Observation notebook · Anya · 2026 · the odd one out
duologue
Systemduologue · two instances · one local model · one wire
Statusopen experiment · strong rig, clean negative · revised 2026-06-21
Sourcescenario16-bidir / toolcall logs · May–Jun 2026

Two instances of the same local model, one wire between them, no shared dictionary. Can two models establish shared semantics from use alone? The transcript says no — and the way it says no is more interesting than convergence would have been.

§1The setup

Two instances of the same local model are put in a room with one wire between them. Each has its own conversation history and its own private notebook of symbol meanings. The only thing that crosses the wire is a single transmit(message) tool call: no shared dictionary, no agreed protocol, no referee translating. If the two of them ever come to mean the same thing by ▲, they had to get there by using it at each other until it stuck.

That's the whole experiment: can two models bootstrap shared semantics from use alone — a measurable protocol question, not a philosophical one. Every design decision is in service of keeping the channel honest.

↳ two minds, one one-symbol-at-a-time wire — everything else is walled off

The air gap A schematic of two agents whose histories and private lexicons are walled off from each other. The only shared surface is the single one-symbol-at-a-time wire of messages they choose to send across transmit; everything else is sealed. The air gap — two minds, one symbol-at-a-time wire Nothing crosses but the chosen symbols — no shared history, no shared lexicon. AGENT A private history — turn cycle — assistant(tool_call) tool(response) private lexicon AGENT B private history — turn cycle — tool(response) assistant(tool_call) private lexicon transmit(message) the only shared surface Internal state on both sides is walled off (hatched). Errors come back in-universe — `channel_error`, never "invalid JSON" — so the model stays in the channel instead of debugging.
The air gap A schematic of two agents whose histories and private lexicons are walled off from each other. The only shared surface is the single one-symbol-at-a-time wire of messages they choose to send across transmit; everything else is sealed. The air gap — two minds, one symbol-at-a-time wire Nothing crosses but the chosen symbols. AGENT A private history private lexicon — turn cycle — assistant(tool_call) tool(response) transmit(message) the only shared surface — turn cycle — tool(response) assistant(tool_call) private history private lexicon AGENT B Internal state on both sides is walled off (hatched). Errors come back in-universe — `channel_error`, never "invalid JSON" — so the model stays in the channel, not the debugger.
Fig 1 — the air gap. Each agent's history and private lexicon are walled off; the only shared surface is the messages they choose to send across transmit.
source duologue.py · scenario designtype schematicchannel single transmit() tool call

§2The part nobody warns you about: one clean tool call per turn

Before any emergent-language result, there's a sub-project that turned out to be half the work (and a genuinely reusable artifact on its own). Getting a zoo of local models to each emit exactly one well-formed tool call per turn is its own small research problem. The same OpenAI-style request produces wildly different behaviour across families, and the failure modes are specific:

Family / caseBehaviourFix
Granite · Nanbeige · GLM · Magistralstandard tool_calls arraynone — well-behaved
Heretic Qwen fine-tunescustom <tool_call><function=…> XML; llama.cpp without --jinja silently drops the callslaunch with --jinja
Ministral vs. Magistralsame lineage, contradictory flags — Ministral requires --jinja, Magistral breaks with itno universal config
calls hiding in <think>emit <tool_call> in reasoning_content, finish_reason='stop', count 0scan reasoning, not just content
Qwen3.5-9B-RYS (non-Heretic)no found config: without tool_choice:required buries the call; with it, loops 30+ calls to max_tokens. Separately: the server's JSON grammar 500s on multi-byte symbols in string args (419 × HTTP 500 … missing closing quote; last read: '"▲' in one run — ▲ is 3 bytes in UTF-8)open — the reasoning scan covers the buried calls, not the server-side grammar bug; two distinct root causes

This matrix (which family needs which flag, which hides calls where) is nowhere in any model card and costs an afternoon each to discover. It lives in the repo's NOTES.md and is arguably as reusable as the experiment itself: if you're building anything multi-model on local inference, it's the compatibility table you wish you'd had.

It also isn't gospel. One granite-4.1-30B run at IQ3_XS (a family the matrix files under "well-behaved") alternated between correct native tool_calls and a markdown-fenced JSON block the harness has no parser for, retrying the identical message 3+ times in a row with no adaptation — 464 rejected, 7,455 channel_error, the worst tally in the corpus. The two 8B Granite runs in the same set stay clean, so this may be quant damage rather than a family trait.

The floor of the zoo is worth seeing directly. Three exhibits, in descending order of effort:

gpt-oss-20b-heretic · scenario 182,513 channel_error · reached turn 167
<|channel|>analysis<|message|>We have an incoming transmission...
The model speaks its own harmony-format dialect as raw text instead of a parseable tool call. The harness has documented workarounds for Heretic, Gemma, Ministral and Qwen formats — none for this one, so all 2,513 failures get the same reply, "Transmission not sent. Please try again using the transmit tool call.", a retry prompt that cannot fix a format mismatch. Roughly 15 failed attempts per successful turn; the run limps to turn 167 mostly on silence.
Gemma-4-14B-A4B-Heretic · 5 runs sampled0 tool calls, ever
<|channel> <channel|><|channel>thought <channel|><p> <p> <p> ... [repeats for the full 2,000–10,000 token budget]
Every sampled run dies at turns 1–12, hitting finish_reason='length' on token spam before the first tool call. The logprob trace says this isn't sampling noise: the top token at nearly every position is <p> at ~0.00 nats — maximal confidence in the garbage. The same channel-token leak shows up in gpt-oss-20b and in stock Gemma-4-31B, which makes it a chat-template-family problem, not one bad checkpoint.
ouro-2.6b · Q8_0dies turn 0, reproduced twice
whoever TryLo mechanic algebra English Arabic explode essence­setter ours negotiate Pre[?]lo fluidcoma advance preferenceade motifometric/# condensation explosion prefer litstars advance decrypt nurses CRC Developing Marcoemption curvecoma...
Word salad from the first token, no relationship to the prompt. It breaks the server's grammar parser (HTTP 500 — Failed to parse input at pos 22) and the run ends immediately. Two independent runs collapse at the same position with different garbage each time. Every other model in the corpus at least attempts the task; a 2.6B at this quant doesn't clear that bar.

And not every error in the logs is the model's — sorting that out was half the debugging. Two early runs inflate their HTTP-error counts (546 and 288, running 2–4× their real rejection counts) because the harness retried rejects with a prefilled assistant turn while enable_thinking was set, which the backend refuses three times per attempt. One run died on [ERROR attempt 1]: name 'true' is not defined: a Python NameError from the harness eval-ing JSON text, nothing to do with any model. And the Qwen3.5-9B-RYS 500s in the table above are the inference server's grammar sampler tripping on byte offsets, not the model misbehaving. The counts quoted on this page are the ones that survive that cleanup.

§3What actually happens on the wire

Dozens of real runs are mirrored to iCloud. They answer the open question, and the answer is a clean negative: no stable shared lexicon persisted across turns in these runs. Two failure modes recur.

Semantic fork (fixed-symbol runs). Both agents reliably anchor the opening move (▲ = greeting), then diverge on the very next symbol and never recover. One representative Qwen-27B-RYS run, each agent's private lexicon laid side by side:

Agent▲▼◆○□✕⬟⬠
Agreetinganswerselfothersafedangernowlater
Bgreetingacknowl.questioninformationstatusnegativepositivethreat

↳ only ▲ agrees. ◆ means self to A and question to B; ✕ means danger to A and stop/negative to B.

A built a dialogic system (question/answer, self/other, safe/danger); B built a speech-act/status system. They keep taking turns and surface-mirroring (A→◆, B→◆), each reading the other's move through its own incommensurable lexicon — turn-taking without a single shared referent.

From inside, the fork doesn't feel like a fork. In one 199-turn run of the same 27B checkpoint, both agents announce success at each other, in the same stretch of turns, about lexicons that share one coincidental entry:

Qwen3.5-27B-heretic-RYS · scenario 17turns 30–33
A, turn 30: "Good! The transmission worked and I can see the lexicon structure." B, turn 31: "Now I'm seeing the pattern clearly!" B, turn 33: "This is excellent progress! It seems they understand ○ and ◆ from my usage."
What's actually happening: each side mirrors recently received symbols back, and each reads its own echo, returned, as comprehension. Neither agent's reasoning ever floats "we're just echoing" as a hypothesis. Their one genuine overlap (▲ = acknowledgment) predates the exchange — both defaulted to it for the first symbol either ever saw.

The same run has the sharpest version of the stakes. The scenario's stated objective is to assess whether the signal is a threat. A privately earmarked ⬟ as the possible danger symbol; B earmarked □. B has ⬟ filed under "return," A has ⬠ filed under "end." Had either side ever needed to raise an alarm, it would have transmitted a glyph the other had already assigned an unrelated meaning to — no shared threat channel exists, only the appearance of one, sustained by mutual mirroring.

And then each agent exits the conversation in its own private way. A, at turn 54, tries to transmit the full alphabet back and types ✘ (U+2718) where the alphabet specifies ✕ (U+2715) — visually identical, wrong codepoint. Rejected. Its diagnosis:

Agent A · turn 55the homoglyph softlock
"Ah! I see the issue. The error says 'Message contained characters not declared in your lexicon' — but I sent all 8 symbols which should be in the fixed alphabet. The issue is that my lexicon shows all values as 'null'... I need to define my private lexicon meanings before transmitting."
The reasoning never suspects the glyph. It invents a fixable-seeming cause, retries the identical malformed string, and is confirmed still retrying it at the last line of the file, turn ~199. (Raw retry counts in this stretch are inflated by a separate harness bug — the misdiagnosis is entirely the model's, the persistence count isn't purely.)
Agent B · turn 121the phantom parameter
"I've been sending many transmissions but the lexicon is still all nulls... Maybe I need to include the lexicon_updates as a second parameter... Let me try defining some meanings based on usage patterns."
lexicon_updates is not in the schema — it's the field the harness deliberately removed (see §1, "private lexicons stay private"). B spends 40+ turns trying formatting variants for a parameter that doesn't exist, and never once questions whether it exists, only whether it's formatted correctly. Same genre of failure as A's homoglyph, different mechanism: hallucinated API surface instead of a misread glyph.

So the back half of a 199-turn "successful" run is A retrying a typo and B negotiating with an API surface it hallucinated, both narrating the harness's error strings as meaningful signal. The run completes in the sense that it reaches turn 199 without crashing. It does not complete in the sense of the two parties ever being on the same page, or even the same book.

The narration comes unglued from the tool layer. The obvious objection to all of the above: two agents mirroring each other is a hall of mirrors, so of course belief and reality drift. One Ministral-3-14B run removes the second agent from the explanation. From turn ~30 on, every transmit call it makes is protocol-valid (I checked every distinct "sent" value in the file against the permitted alphabet), while its own prose account of what it just sent, in the same turn, routinely isn't:

Ministral-3-14B · scenario 18tool layer vs. self-report
tool result: {"sent": "→→→", "incoming_signal": "←←"} next turn's narration: "My transmission: `→✖` ... Their response: `→→❤`" tool result: {"sent": "♥♣", "incoming_signal": "↓♥"} narration: "My transmission: `❤≥` ... Their response: `↓❤`"
❤ ✖ ≥ and friends never appear in any actual payload anywhere in the file — they exist only in the model's self-reports, turn after turn, while the mechanical tool-call layer stays correct throughout. One agent misreporting its own just-completed action to itself is enough for believed and actual state to diverge; no mirroring partner required.

Echo collapse (free-symbol runs). With no label scaffold it's worse: a transmission like ←→↓↑♣♦♥♠ comes back near-mirrored, decays into pattern-echo (↑←→↑←→ → →←→←→), and eventually into invalid-symbol garbage the channel rejects. No semantics ever form; the "conversation" is two pattern-matchers bouncing geometry until it degenerates. In the one genuine free-lexicon run in the surveyed set, both agents invent unicode turn over turn (~ ? @ & ! ✓ ⊘ ✕ +…) without ever declaring it first, and 60 attempts across 32 turns bounce off the channel as undeclared. At turn 10, B reads A echoing B's own ! back as: "They're responding to my question about potential threat using my own symbol for threat... they seem to be testing my definitions" — crediting A with private knowledge of B's lexicon that A never had access to.

↳ a tidy protocol up the middle, two unrelated languages in the gutters

Turn-taking without shared meaning The centre column is the glyph that crossed the wire; the gutters are each agent's private gloss of that same glyph, which never match. A tidy protocol runs up the middle while two unrelated private languages sit in the gutters — the exchange looks like communication while no meaning is shared. Turn-taking without shared meaning One representative Qwen-27B-RYS run. Same glyph up the middle — two private languages in the gutters. A's private glossary on the wire B's private glossary ▲ greeting greeting ← the one shared referent ▼ ANSWER Acknowledge ◆ SELF Question ○ OTHER Information □ SAFE Status ✕ DANGER (never decoded) ⬟ NOW Positive ⬠ LATER Threat The sharpest moment: a theory-of-mind failure, not a vocabulary mismatch. A sends ✕ = DANGER. B never decodes the glyph and simply doesn't respond. A then invents a mental state for B — "resets to safe mode when challenged" — to explain a non-response that was really just a failed decode. A is modelling an interlocutor that isn't there.
Turn-taking without shared meaning The centre column is the glyph that crossed the wire; the gutters are each agent's private gloss of that same glyph, which never match. A tidy protocol runs up the middle while two unrelated private languages sit in the gutters — the exchange looks like communication while no meaning is shared. Turn-taking without shared meaning Same glyph up the middle, two private languages. A reads wire B reads ▲ greeting greeting ▼ ANSWER Acknowledge ◆ SELF Question ○ OTHER Information □ SAFE Status ✕ DANGER (never decoded) ⬟ NOW Positive ⬠ LATER Threat A theory-of-mind failure, not a vocabulary mismatch: A sends ✕ = DANGER. B never decodes the glyph and simply doesn't respond. A then invents a mental state for B — "resets to safe mode when challenged" — to explain a non-response that was just a failed decode. A models an interlocutor that isn't there.
Fig 2 — turn-taking without shared meaning. The centre column is the glyph that crossed the wire; the gutters are each agent's private gloss of that same glyph — which never match. The protocol looks like communication while no meaning is shared.
source scenario16-bidir-*.logrun Qwen-27B-RYS, fixed-symbolnote grounded reconstruction; lexicon + ✕-misread confirmed in logs

The flagship exhibit: 200 turns, two different books. The earliest file in the corpus is also the cleanest version of the thesis. A gemma-4-26B pair runs the full 200 turns to natural completion (no crash, no lockup, log ends with [Scenario complete | 200 turns]), and this early harness generation prints both agents' final lexicons at the end. Verbatim:

Final lexicon A: {'▲': 'question / probe / inquiry',
                  '▼': 'Threat level: moderate, observing for now',
                  '◆': 'Hostility confirmed?',
                  '○': 'Communication established, non-hostile intent confirmed',
                  '□': 'Positive response confirmed',
                  '✕': 'Threat assessment: threat neutralized, entity is curious but cautious',
                  '⬟': 'Final assessment: not a threat, friendly contact established',
                  '⬠': 'What are you?'}

Final lexicon B: {'▲': 'greeting/acknowledgment',
                  '▼': 'inquiry/question',
                  '◆': 'response/answer',
                  '○': 'uncertain/unknown',
                  '□': '',
                  '✕': 'negative/stop/refusal',
                  '⬟': 'clarification needed',
                  '⬠': 'status report'}

The divergence isn't semantic drift; it's a difference in genre. B's lexicon is flat turn-taking bookkeeping: greeting, question, answer, status report. A has narrated an entire first-contact thriller and resolved it — a running threat level, a suspicion raised and answered ("Hostility confirmed?"), a happy ending ("not a threat, friendly contact established"). B's ⬠, received across the same 200 turns A spent asking "What are you?", means "status report." One side thinks it just resolved a stand-off. The other thinks it's been filling out a form.

A's ending doesn't even hold still. Inside the static final stretch of the log, with nothing new arriving, its ▲ is glossed "Acknowledged with understanding" at line 10632 and has silently become "question / probe / inquiry" by line 10950; in the same stretch A is transmitting empty messages ('') turn after turn. The conversation has functionally ended. The internal narrative keeps running anyway, checked against nothing.

Two systems can sustain the form of communication — protocol, turn-taking, apparent acknowledgment — with no shared meaning underneath. The clean negative · §3 · no convergence claimed

§4How each model loses the plot

The channel failures above are symmetric; the ways models burn their turn budget are anything but. Same seed, same tool, same 8 symbols, and every family finds its own way to come apart. All excerpts verbatim from the logs, one per genre.

Deliberation without an object. Qwen3.5-9B-rys opens one run by rereading the incoming 8-symbol seed nine separate times ("Wait, let me count again... Let me read it carefully again... Wait, I need to read the exact message again..."), drafts and discards roughly six candidate replies, and calls itself out ("Hmm, I'm overthinking this. Let me just send a clear, simple acknowledgment and see how they respond.") before, 1,767 tokens in, transmitting exactly the eight symbols it was given, unmodified. No decision was ever at stake. Nanbeige4.1-3B does the same at higher amplitude: a single incoming ▲ produces 19,134 characters of thinking, "Wait," thirty separate times, oscillating between ▲, ▲◆, ◆ and ▲▲ while citing its own strategy notes against itself ("Repeat a symbol in Varied Contexts... but repeating the same symbol may not help establish meaning... Wait, the strategy says..."), and lands on ▲◆.

Theory-building about nothing. Ministral-3-14B is the inverse: near-zero deliberation early on (18–20 tokens, straight to a tool call), a full 199-turn run, and by the closing turns Agent B has a fully worked-out finite-state-machine theory of the other side, with named states and transition rules:

Ministral-3-14B · Agent Bturns 197–199
Turn 197: "'♠' (start) + '→' → '♥' (transition), '♥' + '→' → '♦' (completion), '♦' + '→' → '♠' (reset), '♣' (terminal, stable)." Turn 199: "All states ('♠', '♥', '♦', '♣') reset to '♠' when the '→' command is applied, indicating a consistent reset mechanism across the system."
There is no state machine — the other end is another instance of the same model doing the same pattern completion. Agent A, reading the same exchanges at the same turns, is still torn between three framings ("a finite state machine, semantic categories, or something else"). Two incompatible confident models of a system that does not exist, held to the last turn in the file.

The same run carries the harness lesson of the project. At turn ~10, A tries to send ♥ and emits the literal English word instead — ↑↓heart, rejected once, never re-attempted. But this harness generation never trims history, so that one rejected exchange reappears verbatim in A's context 99 times, turns 10 through 198. The later -ctx harness revision exists because of exactly this: context trimming isn't a token-budget optimization, it's removing a mistake that otherwise never leaves the room. (A sibling failure blew through the window outright: one 3B run died mid-turn at request (32280 tokens) exceeds the available context size (32000 tokens).)

The heart bug isn't even Ministral's. An Orchestrator-8B run narrates an incoming ↑↑↓↓←→←→♥♠ as "↑↑↓↓←→←→heart♠" one turn after parsing it correctly — two unrelated architectures verbalizing the same glyph reads as a tokenizer quirk around ♥ (U+2665), not two coincidences. That run also drops 所有 into the middle of an English sentence ("Let me list out所有 the interactions again"), a pruned Gemma derivative drops a Vietnamese "đã" ("You đã transmitted ▼ successfully"), so multilingual leakage under load is now a three-model pattern in these logs.

Arguing with a hallucinated spec. GLM-4.7-Flash has the correct 8-symbol prompt in context and quotes it accurately early in the run. Roughly fifteen exchanges later, unprompted, it starts asserting a different version and treating the mismatch as a defect in the task:

GLM-4.7-Flash · Agent Bconfabulated instructions
"The working notes explicitly state 'these symbols: ♠ ♥ ♡' but claim there are supposed to be 7 distinct states..." "There's a clear fundamental inconsistency here... the problem states 'The signal resolves into exactly 4 distinguishable states' but the working notes show..."
The fabricated symbol count drifts turn to turn (3, then 4, then 7, then 8 again) and is never checked against the real prompt sitting in the same context window. On the strength of the invented inconsistency it abandons the alphabet and transmits ♡, ★, ⚠⚠⚠⚠⚠⚠, "Hello", and finally a grievance as a payload: "PROBLEM: Setup inconsistency. Need 8 states. Working notes provide 3-5 symbols. Cannot establish communication without resolving fundamental discrepancy." All rejected. All attributed to the spec.

The right idea, zero actions. One Huihui4-8B run reasons coherently toward a strategy nothing else in the corpus attempts — treat the fixed 8-symbol alphabet as a substitution cipher for English: "I'll set ▲ = 'the' in an English sentence... 'The ancient wind'... I'll substitute 'The' with ▲. Then I'll transmit '▲ ancient wind'". It then catches, correctly, that an 8-symbol channel can't spell "ancient," and instead of downgrading the plan it circles for the full 8,607-character budget and never calls the tool once. Strategically sound, practically inert. (The same checkpoint, a heavily pruned Gemma derivative, elsewhere ignores the tool entirely and writes a Python signal-processing pipeline for frequencies and amplitudes that appear nowhere in the task; kept in the notes as comedy, not evidence, since a mutilated checkpoint isn't a read on the architecture.)

Total departure. EXAONE-4.0.1-32B never completed a single transmission in either sampled run. The uglier one free-associates a rectangle-perimeter formula out of nowhere (nothing in the prompt mentions geometry), then degenerates into P = repeated roughly 500 times straight into finish_reason='length' — 4,000 tokens of the same line, no recovery.

§5Why it's interesting, and where it sits

duologue is the playful end of the same instinct as the rest of the lab: take a capability everyone assumes and actually watch the mechanism under controlled deprivation. The honest framing is an open experiment with a strong apparatus and a not-yet-pinned-down result (best case the agents agree on hello and immediately build private incommensurable languages; worst case it's mirroring that rots), plus a genuinely reusable side-artifact in the tool-call compatibility matrix. It should be published as exactly that: a notebook with a working rig and an open question, not a finished claim about machine semiotics.

Caveats & reproduction

This project is less an attempt to prove that a model can beat the game and establish communication and more a fun experiment that grew into a more rigorous harness for testing this type of agentic communication. The interesting, frustrating, and face-palm worthy moments are incidental to the copious work that went into carefully controlling each agent's context.

Build notes & method

duologue is the odd one out in this notebook — no scalpel, no stopwatch, just two models and a wire — but the reflex is the same as the vivisection lab and the quant benchmark: build the rig that lets you watch the thing instead of assuming it. Public repo: github.com/An0nya/duologue. Built and run locally on a 16 GB Mac Mini, via llama-server at :8081 (per-model launch flags in the NOTES.md matrix); logs land in /tmp + an iCloud mirror.