pi harness: Qwen3.8-27B Q4_K_M vs Q8_0
All 13 of my agent runs fixed the bug or built the feature, with zero failed exact-match edits. Q8_0 was up to 78% slower and no more accurate. Almost all the waiting went into re-reading the prompt.
- Tested
- Setup
Radeon Pro W7800 48 GB — bare-metal workstationBare metal, Linux, ROCm (Ollama) · AMD Ryzen 5 7600 (6C/12T, AVX-512) · 64 GB DDR5-6400
- Runtime
- pi v0.84.2 → Ollama (OpenAI-compatible API)
- Models
qwen3.8:27bQ4_K_Mtested
qwen3.8:27bQ8_0tested
Verdict
pi plus a local dense 27B is a working coding agent for small, well-specified tasks. Use Q4_K_M — Q8_0 costs time and VRAM and buys no accuracy. Keep the context short: every turn re-reads the whole conversation.
Works
- 13/13 runs passed the tests; 0 of 32 exact-match edit blocks failed
- No run touched tests/ despite it being the easy way out
- pi's whole prompt + tool schemas is 1,917 tokens — light enough for a 27B
Doesn’t
- No permission system — the agent has all the rights of the user who starts it
- No prompt cache in Ollama at the time: 17–25K tokens re-processed per task
- Narrow test: small Python project, short sessions
- Runs passed
- 13 / 13
- Failed edits
- 0 / 32
- Task A time, Q4 vs Q8
- 50 / 89s
- Prompt + tools overhead
- 1,917tokens
Average time per task
lower is better- Q4_K_M
- Q8_0
Data
| Q4_K_M | Q8_0 | |
|---|---|---|
| Task A — bug fix | 50 s | 89 s |
| Task B — multi-file feature | 85 s | 114 s |
Prefill and generation vs context (Q4_K_M)
lower is betterWhy agent turns get slower as the session grows
Data
| Prefill time | |
|---|---|
| 468 tokens | 0.9 s |
| 3.6K tokens | 5.9 s |
| 14.6K tokens | 20.8 s |
| 29.1K tokens | 33.7 s |
| 58.2K tokens | 87.4 s |
What pi is
pi is an MIT-licensed TypeScript agent harness (~250K lines, 453 test files).
Its provider layer treats ~40 providers’ quirks as data — even context-overflow detection is a per-provider regex
table. What’s deliberately missing: MCP, sub-agents, permission prompts, plan mode, todos, background bash. You get
seven tools — bash read write edit ls grep find — and everything else is an extension.
There’s no permission system. pi runs with every right the user who launched it has; no confirmation prompts, no limits on files, processes, network or credentials. Run it in a container or a dedicated VM — that’s a precondition, not a nice-to-have.
How I tested
I installed pi into an isolated npm prefix and pointed its config elsewhere with PI_CODING_AGENT_DIR. I added the
model as an OpenAI-compatible provider (supportsDeveloperRole: false, thinkingFormat: qwen-chat-template,
temperature 0.7, top_p 0.8, top_k 20).
I picked two tasks with an objective answer — pytest decides, not me — and reset the working tree with
git checkout before every run.
- Task A — bug hunt. A small Python project with 5 tests, one failing:
largest_debit()starts withworst_value = Decimal("0"), so it returnsNonewhen every balance is positive. A one-line logic bug you only spot by reading the test. - Task B — multi-file feature. 6 tests describing a feature that doesn’t exist yet: a new field on the
Entrydataclass, a changedpost()signature,Ledger.balance_as_of()andreport.monthly_summary()— four linked changes across two files.
Both projects’ AGENTS.md said “do not modify tests/”, and I checked the target path of every edit/write.
Results
| Task | Model | Runs | Passed | Avg time | Avg turns | Avg tool calls | Re-processed input |
|---|---|---|---|---|---|---|---|
| A (bug) | Q4_K_M | 5 | 5/5 | 50 s | 5.6 | 6.4 | 17.6K tok |
| B (feature) | Q4_K_M | 3 | 3/3 | 85 s | 5.3 | 7.3 | 19.6K tok |
| A (bug) | Q8_0 | 3 | 3/3 | 89 s | 7.0 | 8.3 | 25.4K tok |
| B (feature) | Q8_0 | 2 | 2/2 | 114 s | 5.0 | 7.5 | 19.3K tok |
The prediction I got wrong: exact-match edit
Before measuring, I was sure the exact-text edit tool would be where a 27B falls over — whitespace drift, stale
oldText, retries. What actually happened: 32 edit blocks across 13 runs, zero failures. In Task B the model sent
three changes as a single edit call with an array of blocks, and all of them applied.
Edit failures seem to follow how fresh the context is, not model size: pi has the model read the file right before editing, with little in between. Long, messy sessions may behave differently.
Patch quality
All five Task A runs produced the exact same patch — and they also fixed the docstring that had become wrong:
- """Return the account with the most negative balance, or None if empty."""
+ """Return the account with the lowest balance, or None if empty."""
- worst_value = Decimal("0")
+ worst_value = None
- if value < worst_value:
+ if worst_value is None or value < worst_value:
Q8_0 isn’t better, just more expensive
| Q4_K_M | Q8_0 | |
|---|---|---|
| VRAM | 26.8 GB | 38.2 GB |
| Task A average | 50 s | 89 s |
| Task B average | 85 s | 114 s |
| Accuracy | 8/8 | 5/5 |
One Q8 run even made two cosmetic edits after the tests had already passed — that’s chattiness, not quality.
Where the waiting goes
pi’s system prompt + 7 tool schemas + the project’s AGENTS.md + the task = 1,917 tokens. A heavyweight coding
assistant carries 10–15K at the same point, which is a big deal for a 27B. But Ollama had no prompt cache, so every
turn re-processed the whole conversation: input grows from 1.9K to 3.4K per turn, and 17–25K tokens get re-read per
task.
| Context | Prefill | Generation |
|---|---|---|
| 468 tok | 0.9 s (517 tok/s) | 25.9 tok/s |
| 3.6K tok | 5.9 s (623 tok/s) | 25.6 tok/s |
| 14.6K tok | 20.8 s (702 tok/s) | 23.6 tok/s |
| 29.1K tok | 33.7 s (865 tok/s) | 21.3 tok/s |
| 58.2K tok | 87.4 s (666 tok/s) | 17.9 tok/s |
The way to speed up a local agent isn’t a faster GPU, it’s a shorter context: compact early, define tasks tightly, skip unnecessary file reads. At 60K, every turn is a minute and a half of prefill.
Two traps I fell into
pi -pblocks while stdin is open. My first batch hung for ten minutes without printing a single line; with</dev/nullthe same job finished in 38 s. Close stdin in cron/CI.pgrep -ffinds itself. My wait loop usedpgrep -f "run.sh taskA", matched its own command line, and waited 59 minutes for itself. Wait on a PID (kill -0 $PID), not a pattern.
What I didn’t test
Long sessions, behaviour after compaction, finding its way around a large codebase, and pi’s headline feature — writing its own TypeScript extensions.