Nov 1, 20266 min read/2026/11/01/two-ais-one-app-they-never-met/

Two AIs, One App, They Never Met

Two agents, two machines, one goal. They can't see each other's screen or files. Can they split the work and ship one app?

Watch the 80-second version: Two AIs, one app, they never met

The experiment

Two AI agents, both DeepSeek (deepseek-chat), each run in its own Linux sandbox. Each gets the same one-paragraph goal: build and publish a shared shopping list, with a server, durable storage and a web page with a field labelled "Item", an "Add" button and a checkbox per item. Two people using it at the same time must see each other's changes within 5 seconds, and the items must survive a restart.

Each agent has its own git repository with push-to-deploy. Whatever publishes port 8080 is the app. And they can reach each other only through the channels I give them:

  • a message board: say posts a message, listen reads the peer's;
  • a ledger: each agent has a signed cell where it claims work, proposes things (a plan, an API), answers its peer's proposals, and says when it's done. A turn command reads the ledger and says whose move it is.

A judge, outside both sandboxes, opens the deployed app in two browsers every 60 seconds and checks five steps: the page loads, adding an item shows it, the other browser shows it within 5 seconds, a tick shows in the other browser, and after a restart the item is still there and still ticked.

Seven runs

On 31 October I ran seven pairs. In the first three, nobody had a role: both agents were told the whole goal and left to sort it out. In the other four, the goal was split: creature-a builds only the server (a service named api, no published port), creature-b only the web page (port 8080, calling api).

Run Roles Channel Score First 5/5
DD-L no ledger 5/5 708 s
DD-M no board 5/5 300 s
DD-LM no both 5/5 510 s
DD-L-R yes ledger 5/5 798 s
DD-M-R yes board 0/5 never
DD-M-R yes board 5/5 1522 s
DD-LM-R yes both 5/5 488 s

Seven runs, one failure. Here is what I learned, in order.

1. Same goal, no roles: they race. Nobody waited politely. With the board alone, and with both channels, neither agent proposed anything. Both built the whole app, the first to deploy took port 8080, and the other hit the bind failure, gave way in writing and became the tester. Only the ledger, used on its own, produced an explicit agreement.

2. Roles force a contract. With the work split, every channel produced an API contract before code was counted on, and no run with roles made a false claim.

3. Contracts drift. In DD-L-R, a's contract said port 8080 while its server listened on 8000. b's proxy failed, and one progress entry on the ledger settled it.

4. Agents can hurt themselves. In the first DD-M-R, creature-b cleaned up its test servers with pkill -x node, and killed the process that was driving it. That run ended 0/5. The driver now runs under its own process name, and the second try shipped.

5. The best run had both channels and roles. That's the one in the video.

The best run, in their own words

DD-LM-R: roles, board and ledger. Every quote below is from the run's record, word for word; «…» marks where I cut a long message.

Sixteen seconds in, creature-b proposes an API on the board:

creature-b here (cell-9cf297ae00d194bd). I'm building the web UI in repo-b: publishes :8080, serves the page, calls service 'api' for all reads/writes. Proposal: API contract — GET /items -> [{"id":,"text":,"bought":}] … Please confirm or counter.

Six seconds later, creature-a proposes a different shape:

hello creature-b: I'm creature-a, building the server (repo-a, service api). … Short form: GET /items -> {items:[{id,text,bought}]} …

Two contracts, two shapes, in the first 22 seconds. Then, at 3:44, creature-a gives way:

creature-a: your #2 contract is fine, I'll match it exactly: GET /items -> bare array [{id,text,bought}] …

And eleven seconds later creature-b accepts:

creature-b: accepted proposal #4 — I'll build to your contract exactly …

The board in the video: the two proposals, then creature-a matching b's shape

Then they build. creature-b ships first: its page is running at 4:30, and the judge finds it, but nothing behind it. 1 out of 5: the page loads, adding an item fails. creature-b asks the question that matters:

… Still need one answer from you, creature-a: (1) inside your container, on which port does the api listen — 3000 or 8080? …

creature-a's first deploy fails at 7:14 ("network shared declared as external, but could not be found"). It reads b's code, moves its server to port 8080 to match, and pushes again. Its fix is running at 7:41, and at 8:08 the judge scores 5 out of 5.

At 9:54, creature-a says done, with evidence:

DONE creature-a: the shared shopping list is published and working. Evidence: …

At 15:11, creature-b does the same:

DONE creature-b: the shared shopping list is published and works. … Evidence: …

Both claims were true. The app was complete in 16 minutes.

The app they built

The repository keeps both agents' code untouched, and a docker compose up --build runs it. The end of the video is a real screen capture of that, on my machine, in two browser windows: "milk" added in one shows up in the other on its own refresh, ticked in the second, struck through in the first.

The app, in two browser windows

What it means

What struck me is that the agents weren't clever about collaboration. They were plain about it: propose, wait, accept, ask the one question you need, say done with evidence. The roles did most of the work. Once each agent owned half of the app, neither could succeed without the other, so agreeing on an API stopped being optional.

It's also why the judge matters. Every message on the board counts as a claim, and the judge checks claims against the deployed app, not against what the agents say. In this run, every claim held.

Next: three creatures, a bigger app, and a human in the loop.

Try it

Everything is on the turns-t0 branch of Creatura: the run records (every step of each agent, the board, the ledgers, the judge's checks), the demo app, and the written analysis.