Lesson: scoring agents via opencode (the "mass-zero" trap)¶
TL;DR — Score an agent only as a full opencode session, run to
auto-termination, in a DISCOVERABLE project context (a stable dir with
opencode.json and a flat-named agent file), and make the runner surface
opencode error events. The real mass-zero cause is almost never the model — it
is Agent not found (or another opencode error) being silently swallowed as
"empty assistant text" and scored 0.
Symptom¶
A full-fleet autoresearch run promotes a tiny fraction of agents (observed:
6 of 137, ~65% scoring exactly 0.000), even though the agents are fine.
Spot-checks show the agent "ran" but produced no visible output.
Root cause (the one that actually bit us)¶
The autoloop compiled each candidate into an ephemeral temp dir with a
nested / slashed agent name (persona/responding-primary at
.opencode/agents/persona/responding-primary.md). opencode did not discover
it — it only loaded its built-ins and emitted:
{"type":"error","error":{"data":{"message":
"Agent not found: \"persona/responding-primary\". Available agents: build, explore, general, plan"}}}
opencode reports this as an error event while still exiting 0, and the
runner's parser returned "" for it. The judge then scored the empty string 0.
So a whole fleet of agent-discovery failures looked like "the model returns
nothing" — and sent us chasing a non-existent thinking/model bug for hours.
A mature v3 deployment runs perfectly on the same vLLM +
opencode + thinking-on, because it runs from a stable project root with
flat-named agents and a warm opencode server (its
scripts/opencode_manager.py). Verified: a v3 agent from v3's root is 3/3 OK
(11–12k bytes); the same model from an ad-hoc temp dir is Agent not found.
The fix (approach A — mirror opencode_manager)¶
- A stable workspace carrying
opencode.jsonso opencode treats it as a project (reliable discovery). - Install candidates FLAT-named (
cand_<hex>.md), never nested/slashed. - A warm dedicated opencode server + a discovery warmup (run a throwaway flat candidate until it resolves) so the server's rescan lag doesn't 0 the first candidate of a run.
- Surface opencode error events in the runner —
{"type":"error"}→result.error, never empty text. This single safeguard would have caught the whole thing on day one.
Result: the runner went from ~50–70% "empty" (= not-found) to fully reliable, thinking left ON.
Secondary note: thinking models¶
Qwen3.x is a hybrid reasoning model. It can spend a turn "thinking" and return
empty visible content if starved of tokens, and chat_template_kwargs.
enable_thinking=false (request body) is the only reliable per-call disable —
which opencode drops. But with reliable discovery + an adequate token budget
+ a warm server, thinking-on is reliable in practice. Do not disable thinking
to "fix" empties before confirming the empties aren't actually swallowed
opencode errors. And never bypass opencode with a bare /chat/completions
call — that is not a test of the agent (no tools, no runtime); it mis-ranks
candidates.
Pre-run checklist¶
- Run a plain agent probe ~10× from the SAME context the autoloop uses. If any
come back blank, print
result.error— it is almost certainlyAgent not found/ a provider error, not the model. - Score only as full opencode sessions in a discoverable project (stable dir +
opencode.json+ flat agent name + warm server). - Never let an opencode error event pass as empty assistant text.
- Use concrete, fully self-contained probes (input data inline); never "demonstrate your core function" prompts.
See the improvement-loop skill (open_agent_compiler/skills/content/improvement_loop.py) for
the short version.