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.
- The tool call is an air gap. The agents never see each other's reasoning, history, or lexicon — only what crosses
transmit. After the first-contact seed there are no bare user messages injecting hints. Whatever meaning forms has to live in the symbols. - Private lexicons stay private. An earlier version injected a lexicon table into the tool response; it was removed precisely because it short-circuited the thing being studied. Convergence, when it happens, is earned.
- Errors are in-universe. A malformed transmission comes back as
channel_errororrejected— never "invalid JSON," never the word "tool." Keeping the fiction unbroken measurably changes how the models recover: break character with a stack-trace and the model snaps out to debug; keep it in-universe and it stays in the channel and tries again. - Immersive framing fixes the helper instinct. The naked task makes an instruct-tuned model turn to you — "what would you like me to send?" Wrap it ("your team intercepted a non-terrestrial signal and cannot assist during the exchange") and the model commits to talking to the other agent. The framing is load-bearing, not flavour.
↳ two minds, one one-symbol-at-a-time wire — everything else is walled off
transmit.§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 / case | Behaviour | Fix |
|---|---|---|
| Granite · Nanbeige · GLM · Magistral | standard tool_calls array | none — well-behaved |
| Heretic Qwen fine-tunes | custom <tool_call><function=…> XML; llama.cpp without --jinja silently drops the calls | launch with --jinja |
| Ministral vs. Magistral | same lineage, contradictory flags — Ministral requires --jinja, Magistral breaks with it | no universal config |
calls hiding in <think> | emit <tool_call> in reasoning_content, finish_reason='stop', count 0 | scan 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:
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.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 | ▲ | ▼ | ◆ | ○ | □ | ✕ | ⬟ | ⬠ |
|---|---|---|---|---|---|---|---|---|
| A | greeting | answer | self | other | safe | danger | now | later |
| B | greeting | acknowl. | question | information | status | negative | positive | threat |
↳ 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:
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:
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:
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
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:
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:
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.
- No convergence claim is made — the runs show the opposite, and that negative is the result. The lexicon table in §3 is a grounded reconstruction; the quoted excerpts and the final-lexicon dump are verbatim from the run logs (ellipses marked). None of it implies emergent language was achieved; the excerpts are there to document its absence.
- Raw error counts need the confound table first. Two early runs inflate their HTTP-error tallies through a harness retry bug, one long GLM file may partly reflect a replayed context window rather than 199 real turns of struggle, and the meaning of
channel_errorchanged between harness generations — the numbers quoted on this page are the ones that survive those checks. - The project is currently inactive. Not all models were tested on the most recent iteration of the harness and scenario prompts — there's a real possibility a revisit turns up a more intriguing result.
- Cloud model APIs aren't wired in. Piping in hosted models would raise the ceiling on what "success" could look like here — those models have the best odds of actually establishing shared semantics, which would answer the core question more definitively than the local-only run can.
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.