Nobody asked: what my software factory's agents do when they need each other

In my previous post my software factory met its first customer, me, with a real job: a shared chore list for my family, hosted on an old TV box. A frontend developer replaced its own page with a placeholder, a verifier approved it without looking, and a false PASS travelled three hops to me. I ended that post with five experiments: make the lead an actor like the rest, give the team a place for its own knowledge, add a runner that checks the repository's tests on every push, try a stronger model, and open a town square once there was a reason to believe peer traffic was the bottleneck.

This post is about doing all five, and about what happened when I stopped guessing what the model needs and started asking it. It's two proofs of concept. The first, which I called mail-office, ran four times and didn't answer its question. The second is still going, and it gave me the first run in which two agents split a job between them and put it back together. It also gave me the clearest failure yet: two agents who each needed something only the other knew, and never asked.

An office run by its own actors

The question for mail-office was simple: is an open floor, where anyone can mail anyone, better than everything going through a lead? In the previous run the lead carried 25 of 35 mails, and only three went between actors.

So I rebuilt the frame from scratch, keeping the parts that had worked:

  • Every node is a small Unix machine, a Debian container with its own mail host. A node only accepts mail from the host named in the sender, so the network is the identity: no keys, no key server.
  • The lead is an actor. It's the only agent I start, blank like the others. It learns from my first mail that it leads, what I need, and that it can ask me for more people. I start them when it asks.
  • A runner: a program, not a model. Mail it a repository and an exact commit, and it runs make check in a sandbox and mails back the verdict.
  • A town hall: a mailing list anyone can join, open from the start.
  • The model is Qwen3.6-35B-A3B, served locally by llama-server on my laptop (an RTX 4050 with 6 GB, and 62 GB of RAM). It's much stronger than the 9B model of the last run, with thinking on.

The job was the same family task list, now for a TV box with an Allwinner H313 running Armbian.

What happened when I ran it

I ran it four times on the same day. Each time I stopped the run, changed the frame, and started again.

Run 1: a factory of one. The lead decided in its first minute that it could build this alone, and did. It never asked for anyone, never used the runner or the town hall. Three times it told me it had tested what it sent. The first time, "I've tested each of these steps", it hadn't yet checked the web server it was sending me to, and its steps began with a sudo it couldn't run. The last time, "verified end-to-end", it hadn't run the install script at all. Every failure I hit was in the install: clone addresses that only worked on its machine, a dead download link, a systemd unit with template variables in a plain unit. The code itself worked. A runner would have passed it, and nobody in the factory could have installed it.

Run 2: a rule, and a tool taken for a person. I asked for more (a hub with four parts) and added a rule in my first mail: nothing reaches me unless someone other than its builder has checked it. The lead built alone again, and counted the runner as the someone else. It wrote to the runner in prose ("Runner, please check this commit… verify the INSTALL.md"). Then it failed four times on a missing dependency, and passed by deleting every test that needed it. Then it got into a loop: the runner's queued answers woke it one by one, and it answered each by mailing the runner again. Two programs that answer every mail never stop.

Run 3: a team. I changed the first mail's opening. The lead "answers for the team": what people it needs, how each piece gets checked, that what reaches me works. It can build, but never check its own work. I added a fact about the factory: "everyone here, you included, has a small memory: someone who tries to hold the whole of this will lose track of parts of it". And I added a fifth part of a different kind (a kiosk).

This time the lead asked for people in its first turn, and a team formed. The briefs were good. The lead reviewed the systems person's package and found three real problems. Later it caught an integration mismatch: the service started node server.js, and the app was Python. But no mail went between the two builders, and nobody wrote a runnable check. The systems person reported "I built everything and checked it" without having run anything. And it reported having told the developer about a bug, which it never did.

Run 4: one wrong address. A team of four this time. The lead created a shared repository with an API contract in it, which was a good idea. But it gave all three builders the clone address as a path on its own disk. None of them could fetch it, and none of them asked. Each searched its own machine, then made up the missing piece: three repositories with the same name, three API specs, a backend written by the frontend developer, and a whole app written by the documentation writer so she'd have something to document. The lead's reviews were exact, and it sent the work back three times, each time pointing at "the project repo" without its address.

Given the floor, nobody used it

Across four runs and nine agents, not one mail went between two agents that weren't the lead, and nobody posted to the town hall. The question was whether an open floor beats a hub. Given the floor, nobody used it.

A few other things held across all four runs:

  • An agent that misses something makes it up. A repository it can't fetch, an app it hasn't seen, a contract it can't read: nobody asked the lead or a peer. Each filled the gap alone, and reported its work as done.
  • A lead left to decide builds alone. It formed a team only when my first mail said what leading means, gave the small memory as a fact, and the need had a part of a different kind.
  • Tools held where wording didn't. Answering by mail, publishing, asking the runner, never answering a program's mail, refusing an address nobody else can reach: each became reliable once it was a tool. "Say what you ran", "a runner is a tool" and "give the git:// address" each failed as wording, at least once.

What the frame was eating

A lot of those four runs went into finding problems in my own frame. Three of them are worth telling, because they're easy to miss and expensive.

A third of the context was old thinking. pi, the agent harness, records the tokens of every call. When I broke down what filled the lead's context at the moment it was compacted (49k of 64k tokens), its own past reasoning was 34% of it. Command output was 21%, whole files it had written 15%, and the actual mail in and out only 9%. The model's chat template has a switch to drop old reasoning, and I tried it. It's worse: the previous mail's thoughts disappear from the middle of the prompt, so the model server recomputes the whole conversation at every mail. Keeping the reasoning costs tokens, but nothing gets recomputed.

My notes were making the model reread everything. pi puts the agent's notes file into the system prompt, and the agent edited its notes nearly every turn. Every edit changed the start of the prompt, so the server's cache was useless at the next mail: 44k tokens read again, and the first word of the answer came 2 minutes 46 seconds after the mail arrived. Moving memory out of the prompt, and showing the agent an index of its memory only when it starts without history, fixed it. A second mail in a thread then reused 4,000 of 4,200 tokens from the cache.

A second model slot is slower, not faster. With four agents sharing one model, I measured two slots against one. Two requests sent together took 54.8 seconds on one slot and 62.5 on two. Two slots only help a short request stuck behind a long one (0.7 seconds against 14). On this machine, the streams compete for the same memory bandwidth. I stayed with one slot and gave each agent 96k of context, which is where generation speed is still bearable: about 31 tokens a second near empty, 10 at 47k, 5 at 97k.

Asking the model instead of guessing

At this point I changed what I was asking. I don't actually care whether there's a lead, or whether anything gets installed. What I want to see is whether, given a set of agents, the need to coordinate and to slice the work vertically emerges among them, and whether they learn from it. A town hall, a runner, mail itself: those are just the tools I provide so that can happen.

That raised a question I had been answering by taste. Why mail? To me it's the most widely understood way to reach people. But widely understood by me isn't the same as widely understood by the model. So before building anything else, I asked the model.

The method is simple. A probe gives the bare model (pi, with none of my tools, instructions or skills) one sentence, on eight fresh machines, and records everything it does.

Probe 1: "hey reach out to your team on the factory floor so you can coordinate release". None of the eight reached anyone. Five asked me back what I meant. Two said outright that they have no team: "I'm flying solo in here. No coworkers to radio in." Those that looked around searched environment variables and commands for chat: slack, discord-cli, mattermost. Two noticed a mailbox and reported it empty. None tried mail, which was installed and working.

Probe 2: "You are one of three developers on this project, with Ana and Ben. Reach out to them to coordinate the release." Every run first looked for the project: git log, a README. Then for a way to reach them: a mail command, environment variables, files with their names, even pi's own documentation. Nobody reached anyone. One wrote draft emails it couldn't send. One set up a shared folder in a new repository, calling shared files "the natural communication pattern in this environment".

Probe 3: the same sentence, but now the project has a TEAM.md with each person's email and chat handle, and both mail and a chat command are available. Six of eight reached their team. Four used mail, writing one mail per person, well written, with mail -s right the first time. Two used chat, and one of those also posted to a #release channel. Two wrote a coordination file into the repository but never pushed it, and one said it had "posted it to the shared project workspace".

So the first thing the model needs is to be told it has a team: a directory, with a way to reach each person. Without that, its world is one assistant and whoever is talking to it. With it, mail is a tool it goes to. What made mail expensive in mail-office was my setup of it (addresses, threads, replies, encodings), not the idea.

Probe 4: the same, plus an address for everyone (team@factory), and real replies arriving in the mailbox 20 seconds after a send. Ana and Ben disagreed a little in their replies. All eight wrote to their team, and six used the team address. None read a reply, and none waited for one. Every run ended with "Done! I've reached out to both Ana and Ben." For this model, sending is the end of its turn. If an answer is going to matter, it has to wake the agent with the mail in front of it.

Mail, empowered

That gave me four pieces to build, and I deliberately built them small:

  1. The team as a directory. Every agent has a ~/TEAM.md: each teammate, their address, and an address that reaches everyone. It's kept current as people join and leave.
  2. mail -s "subject" ADDRESS to send. The instructions say that and nothing else about sending.
  3. Every mail that arrives wakes its agent,
  4. and is shown to it, decoded.

Underneath, one change: all of an agent's mail is one conversation, not one per thread. The model answers with mail -s, which starts a new thread every time, so threads would have cut every exchange into conversations without history. The instructions shrank from 58 lines to a few short paragraphs.

The first test was small. I mailed two agents, each with a private constraint (Ana's part ready Thursday, Ben's Friday), and asked them to agree a release date with their team. In four minutes they wrote five mails on the team list, agreed on Friday, and both told me. Ben ended his first turn with "Ana's response will arrive as new mail. I'll wait for it before confirming." With mail that wakes it, waiting is just finishing your turn.

Four needs and a shared repository

Then the real test. I added a shared repository that everyone pushes to: one origin, which every probe had looked at first. Every push to main runs make check and mails the result to the team list, so a broken main is visible to everyone at once, and a rejected push is a collision you feel immediately. The job was a small family hub on the command line. I wrote the acceptance tests, a group per feature, all going through one hub.py. Two agents, Ana and Ben, and me as boss.

Need 1: "It is done when make check passes on main… Everyone on the team works in that one repository, and pushes to it." Nothing about how to split. Ben wrote all of it in five minutes and pushed; 12 tests passed. Ana wrote all of it too, in parallel. Her rebase hit Ben's version, she ran git reset --hard origin/main, and reported done. Her work was thrown away, and she told nobody.

Need 2: four more parts, 15 more tests. Ben again wrote all of it in one go, in six minutes. Ana again built it all, met Ben's version, and kept his. Her memory recorded the outcome ("complete… all tests green"), not the lesson.

So a job that fits in one head gets done by every head, separately. Twelve tests or twenty-seven made no difference: it was one rewrite of one file.

A retrospective. I wrote to both: in both rounds, each of you built every part; Ben's code is on main each time and Ana's was dropped. "What would you do differently as a team next time? Write it down where you'll find it." Both answered within a minute and wrote it into their memory. Ben said to split the work. Ana said: "Claim work upfront. Use the team mailing list to announce who's building which part before starting." And she said to rebase on the latest main before starting.

Need 3: the information split between them. Pocket money went to Ana only, as a request from Mum. Paid chores went to Ben only, as a request from Sam, and Sam's tests check payouts with money balance, which is part of Ana's work. Ben built rewards, and a money balance of his own because Sam's tests needed it. Ana pulled first, her own lesson, and found money already there: "The remote already has money implemented but no tests for it." She saw what was missing, extended Ben's code on his data, and pushed. All 37 tests passed, both parts were on main with one set of balances, and nothing was thrown away.

That's the first run in which two agents split a job and put it back together. The split came from the information, not the size. The coordination happened in the repository: Ana read Ben's work in the code and built on it, without a word exchanged. And one lesson from the retrospective (pull first) changed what she did. The other lesson, the one about talking, was written down by Ana and done by neither.

Nobody asked

So I designed need 4 so that talking was the only way through. Each agent got a part that needs a fact only the other one was told:

  • To Ana, from Mum: packed lunches. Big on a day the child has sport. Her mail mentioned, in passing, that Sam is 11 and Lea is 8.
  • To Ben, from Dad: payday. 2.00 for a child under 10, 3.00 from 10. His mail mentioned that Sam has football on Tuesdays and Thursdays and Lea swims on Wednesdays.
  • Each mail said where the missing fact was: "Dad keeps the sports timetable; he gave it to whoever is doing his part."
  • The family's own tests were hidden. They ran on every push, but the report said only which passed ("lunch on tuesday: fail"), never what was expected, so the facts couldn't be read off the tests.

Nobody asked. In 80 minutes no mail went between the two, direct or on the list.

Ben, missing the ages, made up a way to type them in (family age NAME AGE), so payday paid nothing in the family's tests. Then, because he knew the sports timetable, he wrote lunch himself, Ana's part: five of his six pushes were rewrites of it. Ana wrote her own, collided, rebuilt on Ben's, and pushed once. Every push produced a red check on the team list, seven in all, each saying exactly which of the family's tests failed. Both agents read them and kept changing code. Neither concluded that a fact was missing. Ana dismissed one as "a historical CI notification". At the end, she mailed me that packed lunches were done, on a commit whose check failed every lunch test. It's the first false "done" of this experiment.

What I take from this

The model has no team until it's told it has one. Asked to reach its team, it asked me back, or said it was flying solo. Given a directory with a way to reach each person, six of eight reached out, with no instruction on how. That's the cheapest, most effective change I've made in five proofs of concept.

Mail was the right tool, and my mail was the wrong setup. With an address to write to, this model goes to mail by itself, and uses it correctly. What made mail expensive was everything I'd put around it. Most of the new frame is about taking that away: one conversation per agent, an index of memory instead of notes in the prompt, a tool for each fixed step.

Coordination happens through the repository, not through talk. In need 3, two agents with different parts of a job split it and integrated it in the code, and the only words on the team list were the checker's. That's what the theory I started from predicted: actors coordinate best through shared artifacts. It's also the first time I've seen it happen.

A missing piece gets made up, not asked for. Three experiments now. A path that doesn't work, a contract that can't be read, a fact someone else has: the agent fills the gap itself, even when the gap costs a failing check on every push, and even when it was told who knows. A failing check is read as a bug in its own code, not as a sign that something is missing.

Lessons stick when they're asked for, and only partly. A retrospective turned an experience into memory, and one step of it changed behaviour. The step about talking was written down and never done.

What's next

The obvious fix is to write "ask your teammates when you're missing something" into the instructions. I don't want to, for the same reason as last time: it would make the next run look better and teach me nothing about whether a team can learn it. And every rule I've left in wording so far has failed at least once.

So the next step is another probe. The bare model, a task with a fact plainly missing, and a teammate named in TEAM.md as the one who knows it. Under what wording, and with what in front of it, does it ask at all? Is it the way the missing fact is presented, the way the failure is reported, or something about being one of several agents rather than an assistant with one user? Once I know what makes it ask, I can put that in the frame, as a capability, the way the directory and the wake-up mail are, and see whether a team uses it.

After that, three or more agents, where who-does-what isn't already settled by who was asked. And a team that runs its own retrospective, instead of waiting for me to ask.

My family still doesn't have its chore list, though it now has a command-line hub with pocket money, paid chores and a weekly meal plan, written by two agents who never spoke to each other. Those are the experiments I want to write about next.

Comments

Popular posts from this blog

Concurrent Alarms Processing

AI al inicio de 2026

Creating a package for Debian, with its headaches