Can a Llama, or a model with no tool-calling, drive a BUILD run? (2026-10-23)¶
Evidence for ADR-0093, #1662. The question was whether Llama "actually drives a BUILD run well", and whether the text tool-call protocol makes the families without native tool-calling usable for BUILD.
Method¶
One small, real task through the real qwenloop runner, on a fresh git repository per trial. The
plan asks for slugify.py (slugify(text) -> str, lowercase, runs of non-alphanumerics become one
hyphen, none at the ends) and test_slugify.py (at least four plain asserts), to be run until they
pass. The result is verified by the harness, not by the model: it imports slugify.py and checks
six cases the model never saw ("Hello, World!", " Already-slug ", "A_B c", "---",
"Python 3.12", ""). A model whose own tests pass and whose function is wrong counts as wrong; one
did exactly that. Cap: 25 turns and 420 seconds per trial. Ollama 0.40.9 on a 25.8 GB Apple machine.
Every model was run through a derived Ollama model with num_ctx 32768, the context the runner is
built for. This is not a detail: the first Llama run used Ollama's default 131,072-token context
for llama3.1:8b, a 23 GB footprint split 23% CPU / 77% GPU, and its first turn took 495 seconds.
That run was discarded as confounded.
Results¶
| Configuration | Trials | Correct | Files made | Avg tool calls | Avg seconds |
|---|---|---|---|---|---|
gpt-oss:20b, native (the default engine) |
3 | 1 | 3 | 15 | 91 |
llama3.1:8b, native tool-calling |
3 | 0 | 3 | 19 | 328 |
llama3.1:8b, text protocol |
5 valid | 0 | 2 | 2 to 22 | 327 to 420 |
deepseek-coder:1.3b, auto (no tool-calling) |
3 | 0 | 0 | 0 | 142 |
Six Llama text trials were run. One crashed on the runner, not the model: Llama asked to read the
absolute path /test_slugify.py and a refused path ended the run. That was a runner bug, since fixed
for every tool (a refused call is now an answer), and the three trials after the fix are the three
that count with the first two. It is reported here as invalid, not hidden.
What it shows¶
- Llama 3.1 8B does not drive this task well. It calls tools sensibly (
write_file,shell,edit_file), but writes wrong code and does not notice, in one case passing its own wrong tests. - The text protocol works and does not help.
autoswitched DeepSeek to it with the notice; Llama ran up to 22 tool calls through it; it is about twice as slow per turn; no outcome improved. - A 1.3B model never wrote a call the parser could recover, in 8 turns, three times.
- The default is weak too.
gpt-oss:20bwas correct 1 time in 3, failing by an underscore bug, by running out of turns on a syntax error, and passing once. This task is not easy.
Limits¶
One task, one machine, three trials per configuration. Differences this small are a rough signal,
not a ranking, and 0 of 5 does not prove a model cannot. It does show that a flag saying a model
can call tools is not evidence that it builds well, which is why vibey models reports only what
Ollama says and what a run shows.
Reproducing¶
The harness, as run. It needs the qwenloop tenant on PYTHONPATH and the derived models above.
import json, os, subprocess, sys, tempfile, time, pathlib, importlib.util
TENANT = "/Users/adam/git/vibey-storm/text-protocol/src/vibey_runners/qwen"
PLAN = """Create two files in the current directory.
1. slugify.py defining slugify(text: str) -> str. It lowercases the text, replaces every run of
characters that are not letters or digits with a single hyphen, and removes hyphens from the
start and end. slugify("Hello, World!") returns "hello-world".
2. test_slugify.py with at least four assert statements about slugify, runnable with
`python test_slugify.py` (plain asserts, no pytest, exit code 0 when they pass).
Run `python test_slugify.py` and fix anything until it passes. Then finish.
"""
CASES = [("Hello, World!", "hello-world"), (" Already-slug ", "already-slug"), ("A_B c", "a-b-c"),
("---", ""), ("Python 3.12", "python-3-12"), ("", "")]
def run(model, protocol, max_turns, wall):
work = pathlib.Path(tempfile.mkdtemp(prefix="bench-"))
subprocess.run(["git", "init", "-q"], cwd=work); (work / "plan.md").write_text(PLAN)
subprocess.run(["git", "add", "."], cwd=work); subprocess.run(["git", "-c", "user.email=a@b", "-c", "user.name=b", "commit", "-qm", "i"], cwd=work)
env = {**os.environ, "PYTHONPATH": f"{TENANT}/src", "QWENLOOP_MODEL": model}
cfg = work / "cfg.toml"; cfg.write_text(f'tool_protocol = "{protocol}"\n')
env["QWENLOOP_CONFIG"] = str(cfg)
cmd = [sys.executable, "-c", "from qwenloop.cli.app import app; app()", "run", str(work / "plan.md"), "--cwd", str(work),
"--backend", "openai-compat", "--base-url", "http://127.0.0.1:11434/v1", "--max-turns", str(max_turns)]
t0 = time.time()
try:
p = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=wall)
code, err = p.returncode, p.stderr[-300:]
except subprocess.TimeoutExpired:
code, err = "timeout", ""
secs = round(time.time() - t0)
turns = calls = 0; names = {}
for ev in work.glob(".qwenloop/runs/*/events.jsonl"):
for line in ev.read_text().splitlines():
e = json.loads(line)
if e.get("type") == "turn.completed": turns += 1
if e.get("type") == "tool.call": calls += 1; names[e["name"]] = names.get(e["name"], 0) + 1
# independent verification
ok_files = (work / "slugify.py").is_file() and (work / "test_slugify.py").is_file()
correct = tests_pass = False; wrong = []
if (work / "slugify.py").is_file():
try:
spec = importlib.util.spec_from_file_location("slugify_under_test", work / "slugify.py")
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
wrong = [(i, o, m.slugify(i)) for i, o in CASES if m.slugify(i) != o]
correct = not wrong
except Exception as ex:
wrong = [("import", "ok", repr(ex)[:80])]
if (work / "test_slugify.py").is_file():
r = subprocess.run([sys.executable, "test_slugify.py"], cwd=work, capture_output=True, timeout=30)
tests_pass = r.returncode == 0
n_asserts = (work / "test_slugify.py").read_text().count("assert ")
else:
n_asserts = 0
return dict(model=model, protocol=protocol, exit=code, secs=secs, turns=turns, tool_calls=calls, tools=names,
files=ok_files, behaves=correct, own_tests_pass=tests_pass, asserts=n_asserts, wrong=wrong[:3], stderr=err.strip().splitlines()[-1:] , dir=str(work))
if __name__ == "__main__":
model, protocol = sys.argv[1], sys.argv[2]
print(json.dumps(run(model, protocol, int(sys.argv[3]), int(sys.argv[4]))))