JJoeven

Projects/Multi-Agent Software Team/Part 2

Shared Repo and Test Oracle

Implement the repo dict, path allowlists, full-file writes, and a fake test runner that execs fizzbuzz in an isolated namespace.

The shared repo is a versioned dict. Every write produces a new snapshot you can put in the trace (or a diff of paths). This part builds apply_patch, run_tests, and the read-only test spec. No git. Git is a later adapter.

Patch API

apply_patch(repo, role, path, content) -> {ok, error, repo}

  • path in ALLOW[role]
  • content is str, len ≤ 4000
  • path matches ^[a-zA-Z0-9_./-]+$ and does not contain ..
  • Copy repo (dict(repo) is shallow; values are strings so OK) and set the path
  • Return error dicts, do not raise, so the supervisor can tell the agent "patch rejected"

Test oracle

Read src/fizzbuzz.py, exec into ns = {}, get ns["fizzbuzz"], run cases. Catch SyntaxError, KeyError, TypeError. Result:

{"passed": int, "failed": int, "errors": [str], "ok": bool}

ok is failed == 0 and errors empty and passed == len(cases).

Do not exec the tests file. Agents might try to rewrite it. The oracle is code you wrote in the supervisor module.

Isolated exec

Use a fresh dict. Do not pass __builtins__ unrestricted if you can avoid it — in CPython you can set {"__builtins__": {"range": range, "str": str, ...}}. In Pyodide this still is not a security boundary against a determined model, but it documents intent. Part 5: never exec model code with real builtins on a machine with secrets.

Live Pythonpython
Output
Run to execute this in your browser. Nothing is sent to a server.

Step-by-step environment rules

  1. One repo object passed into every role. Roles return patches, they do not close over a global if you can help it (a global is OK in Try it; prefer arguments).
  2. Tests are code in the supervisor, spec text in the repo for the planner to read.
  3. Forbidden writes return an observation the agent can see: forbidden_path. A fake coder that tries to cheat should fail the eval.
  4. Deterministic fizzbuzz. No random. Same repo always same test result.

Why full-file writes

Unified diffs are painful to parse when the model miscounts lines. Full-file replacement needs a size cap. For a one-function repo it is the right trade. If you add more files later, the coder patches one path per turn (supervisor rule) to avoid nuking PLAN.md.

Copying, identity, and traces

dict(repo) is a shallow copy. That is correct because values are strings. If you later store nested dicts (a parsed AST cache), a shallow copy will leak mutations across "snapshots." Prefer copying the values you mutate. After each successful patch, append {path, n_bytes, sha256[:12]} to the trace — not the full file. Full files belong in the repo; traces belong in logs.

When a patch is rejected, do not keep a half-applied repo. apply_all should apply sequentially and stop on first error, returning the last good snapshot. Partial applies are how PLAN.md updates while src stays stale and you debug ghosts.

Exercise

Add a second allowed file src/util.py that the coder does not need. Write a test that a planner patch to src/fizzbuzz.py is forbidden. Print both errors.

Check your understanding

Why is the test file not the oracle?