JJoeven

Projects/Multi-Agent Software Team/Part 1

Overview and Architecture

Split planning, coding, and review into roles that share a repo dict and a test oracle, and define stop as tests-pass — not as 'the coder felt done'.

A multi-agent software team is not three chat windows. It is one shared state, narrow roles, and a stop condition the coder cannot lie about. The state is a repo: a dict of path → file text. The oracle is a fake test runner that executes predicates on that dict (or exec of tiny functions in a sandbox you control). Stop when tests pass. That is the same idea as TDD, with extra LLMs.

This project is advanced because coordination fails in new ways: the planner never yields, the coder ignores the plan, the reviewer nits forever, everyone rewrites the same function, tests never run, tests run against stale files. You will crush those with turn-taking, budgets, and the test runner as the only success signal.

The product

User goal: Implement fizzbuzz(n) in src/fizzbuzz.py so tests in tests/test_fizzbuzz.py pass. The repo starts with a failing stub and a frozen test file the coder cannot edit (permission in code). Agents:

  • Planner — writes PLAN.md with steps; does not edit source.
  • Coder — patches allowed paths.
  • Reviewer — posts comments in REVIEW.json; may request changes; cannot patch tests.

A supervisor (your Python, not an LLM) decides whose turn it is, runs tests after coder turns, and stops.

Why a supervisor is not a fourth "agent"

If the model chooses the next speaker without rules, you get a talk show. Route in code:

  1. Planner once (or until PLAN.md exists and is non-empty).
  2. Coder.
  3. Run tests. If pass → stop success.
  4. Reviewer. If approve and tests pass → stop. If request_changes → coder.
  5. Repeat until max_rounds.

The supervisor is a workflow. The agents are policies inside slots. This is how serious systems look (even when marketed as swarms).

Shared state

`` repo = { "src/fizzbuzz.py": "...", "tests/test_fizzbuzz.py": "...", # read-only for agents "PLAN.md": "", "REVIEW.json": "[]", }

text

Files are strings. Patches are `{"path": ..., "content": ...}` full-file writes (no unified diff parser in the browser). Full writes are easier to validate: path allowlist, size cap, test path forbidden.

## Architecture table

| Role | Reads | Writes | Success signal |
|---|---|---|---|
| Planner | goal, tests (read), src | `PLAN.md` only | plan length > 0 |
| Coder | all | `src/*.py` | tests will judge |
| Reviewer | all | `REVIEW.json` | `approve` or `request_changes` |
| Supervisor | all | none (except traces) | tests pass or budget |

import json

GOAL = "Implement fizzbuzz(n) so tests pass."

repo = { "src/fizzbuzz.py": "def fizzbuzz(n):\n return str(n)\n", "tests/test_fizzbuzz.py": "READ ONLY: expect 3->Fizz, 5->Buzz, 15->FizzBuzz, else str(n)", "PLAN.md": "", "REVIEW.json": "[]", }

ROLES = ["planner", "coder", "reviewer"] ALLOW = { "planner": {"PLAN.md"}, "coder": {"src/fizzbuzz.py"}, "reviewer": {"REVIEW.json"}, }

def architecture(): return { "goal": GOAL, "roles": ROLES, "shared": list(repo), "oracle": "run_tests(repo)", "stop": ["tests_pass", "max_rounds"], "supervisor": "python, not an LLM", "allow": {k: sorted(v) for k, v in ALLOW.items()}, }

print(json.dumps(architecture(), indent=2)) print("initial src:\n", repo["src/fizzbuzz.py"]) print("coder must not write tests:", "tests/test_fizzbuzz.py" not in ALLOW["coder"])

text

## Failure modes to design against

- **Planner-as-coder.** Allowlist stops it.
- **Infinite review.** Max reviewer nits = 2, then supervisor runs tests anyway; if green, ship.
- **Coder deletes tests.** Path forbidden.
- **Tests never run.** Supervisor always runs after coder.
- **Shared memory via vibes.** The only memory is `repo` + a `trace` list. No hidden globals in role functions except what you pass in.

## Fake tests are still tests

You will not use pytest in Pyodide. `run_tests` imports the source string via a tiny `exec` into a dedicated dict namespace and checks return values. That is a sandbox with sharp edges (`exec` is dangerous with **untrusted** code). Here the coder is your fake model producing a known function. Part 5 discusses not exec'ing live-LLM code in unsandboxed browsers. For this academy box, exec a **whitelist of names** (`fizzbuzz` only) after a syntax check.

> **Tip:** The test file in the repo can be documentation for the planner while the real oracle is Python in the supervisor. Two layers: agents read specs; code grades.

## Why split three roles at all

A single coder agent can pass fizzbuzz. You split roles to practice **interfaces**: the planner's only legal write is a markdown plan; the reviewer's only legal write is a verdict object; the coder is the only role that can change behavior. That is how you will later map to GitHub: issue/plan comment, pull request, CODEOWNERS review, CI. If two roles can write the same path, you do not have roles. You have a race.

In interviews, "we used a multi-agent framework" is not a design. "CI is the oracle, the model cannot patch workflows, the supervisor is a state machine" is a design. Draw the allowlist table on a whiteboard before you import AutoGen.

## What "done" is not

Done is not `PLAN.md` containing the word complete. Done is not a reviewer emoji. Done is not a coder thought that says "this should work." Done is `run_tests(repo)["ok"] is True` or a budget failure you can show in a trace. Everything else is narration.

## Exercise

Write `run_tests(repo)` on paper for fizzbuzz cases `(1, '1'), (3, 'Fizz'), (5, 'Buzz'), (15, 'FizzBuzz')`. The stub should fail 3 of 4. You will implement it in part 2.

Who is allowed to declare the team done?

  • The coder's thought
  • The planner
  • *The supervisor when run_tests returns pass (or a budget stop)
  • Anyone who writes DONE.md

explain: Success is the oracle, not a role's self-report. The supervisor runs tests and stops the loop.

text