JJoeven

Projects/Multi-Agent Software Team/Part 3

Planner, Coder, and Reviewer Policies

Implement three fake role policies with narrow outputs: a plan, a source patch, and a structured review — then take turns in code.

Each role is a function (goal, repo, trace) -> {role, patches, message}. Fake policies are rule-based so the supervisor is the hard part. Later, each function becomes an LLM call with a different system prompt and the same output JSON schema.

Output schema (all roles)

`` { "role": "planner|coder|reviewer", "thought": str, "patches": [{"path": str, "content": str}], "message": str }

text

Reviewer additionally puts JSON in `REVIEW.json` content: `{"verdict": "approve"|"request_changes", "notes": str}`. The supervisor reads that file after the patch, not the thought.

## Fake planner

If `PLAN.md` empty, write three bullets: implement modulo 15/3/5; keep other numbers as str; do not touch tests. If plan already exists, return no patches (no-op). Prevents planner loops.

## Fake coder

Read tests spec + plan. Write the correct `fizzbuzz` (you may hardcode the known solution in the fake coder — this is a **coordination** project; the live LLM coder is the future swap). If last review is `request_changes`, still write the correct function (fake coder is obedient).

A more interesting fake coder **v1**: first turn writes a version that only handles 3, second turn (after failing tests or review) writes the full version. That exercises rounds. We will do that: **two-phase coder**.

## Fake reviewer

If tests last result in trace is `ok`, approve. Else request_changes mentioning the first error string. If no test result yet, request_changes "run tests" — but the supervisor runs tests without asking, so the reviewer should see a trace event `tests`.

import json

def planner(goal, repo, trace): if repo.get("PLAN.md", "").strip(): return {"role": "planner", "thought": "plan exists", "patches": [], "message": "skip"} plan = ( "- Implement fizzbuzz(n) in src/fizzbuzz.py\n" "- 15 FizzBuzz, 3 Fizz, 5 Buzz, else str(n)\n" "- Do not edit tests\n" ) return { "role": "planner", "thought": "write plan", "patches": [{"path": "PLAN.md", "content": plan}], "message": "planned", }

def coder(goal, repo, trace): test_events = [t for t in trace if t.get("type") == "tests"] failed_before = bool(test_events) and not test_events[-1].get("ok") if not failed_before: src = "def fizzbuzz(n):\n if n % 3 == 0: return 'Fizz'\n return str(n)\n" thought = "first cut: only Fizz" else: src = ( "def fizzbuzz(n):\n" " if n % 15 == 0: return 'FizzBuzz'\n" " if n % 3 == 0: return 'Fizz'\n" " if n % 5 == 0: return 'Buzz'\n" " return str(n)\n" ) thought = "fix Buzz and FizzBuzz" return { "role": "coder", "thought": thought, "patches": [{"path": "src/fizzbuzz.py", "content": src}], "message": thought, }

def reviewer(goal, repo, trace): tests = [t for t in trace if t.get("type") == "tests"] if tests and tests[-1].get("ok"): verdict = {"verdict": "approve", "notes": "tests green"} else: err = tests[-1]["errors"][0] if tests and tests[-1].get("errors") else "no tests yet" verdict = {"verdict": "request_changes", "notes": err} return { "role": "reviewer", "thought": verdict["verdict"], "patches": [{"path": "REVIEW.json", "content": json.dumps(verdict)}], "message": verdict["notes"], }

# smoke: planner then coder v1 repo = { "src/fizzbuzz.py": "def fizzbuzz(n):\n return str(n)\n", "PLAN.md": "", "REVIEW.json": "[]", } trace = [] p = planner("fizzbuzz", repo, trace) repo = {repo, {x["path"]: x["content"] for x in p["patches"]}} c = coder("fizzbuzz", repo, trace) repo = {repo, {x["path"]: x["content"] for x in c["patches"]}} print("PLAN.md:") print(repo["PLAN.md"]) print("src first cut:") print(repo["src/fizzbuzz.py"]) print("reviewer without tests:", reviewer("g", repo, trace)["message"])

text

## Step-by-step role design

1. **Same JSON out.** Supervisor applies patches through `apply_patch` — roles never assign `repo[path]` themselves.
2. **Thoughts are traces**, not control.
3. **Two-phase coder** proves that **test events in the trace** change behavior. That is the multi-agent equivalent of ReAct observations.
4. **Reviewer is cheap.** It does not rewrite source. If your reviewer is also a coder, you built two coders and a race.

## Prompts you would use for real LLMs (do not paste secrets)

- Planner: "Only write PLAN.md. Steps must mention the test spec. No python."
- Coder: "Only write src/fizzbuzz.py. Match PLAN.md and the spec. JSON patches only."
- Reviewer: "Only write REVIEW.json with verdict. If tests ok, approve even if style is ugly."

Style-nit reviewers are why teams never ship. Cap comments; supervisor prefers green tests.

> **Warning:** Do not let all three roles share one transcript without role tags. They will imitate each other and the planner will emit Python.

## Observation channels per role

Do not give every role the same view of the world. The planner needs the goal and the test spec. The coder needs the plan, the current source, and the last test errors. The reviewer needs the diff (or full src on a tiny repo), the test report, and the plan — not the coder's self-congratulating thought. Narrow views cut imitation and token cost.

When you swap in LLMs, this becomes three system prompts and three **trimmed** transcripts. The supervisor still holds the full trace. That split — full trace for ops, trimmed views for models — is the same idea as RAG: not everything belongs in the next prompt.

## Exercise

Add a `nits` counter: if reviewer has requested changes 3 times and tests are still failing, message `escalate`. If tests are green, approve even if the plan is ugly. You will wire this in the supervisor next.

What should the reviewer write when tests are green but the plan is two lines?

  • Request a rewrite of the coder
  • *Approve — the oracle is tests, not prose quality, unless you added a style grader
  • Delete PLAN.md
  • Patch fizzbuzz to add comments

explain: Unless style is in the test oracle, green tests mean stop. Reviewer nits are how multi-agent teams burn budget.

text