test: smoke the agent against a real local model, gating the publish - #39
Merged
Conversation
Adds the layer the scripted suite cannot cover: a run driven by an actual model, served by llama.cpp on the runner. No hosted API and no key. Placement is deliberate. A nightly job reports at 3am on yesterday's commit and nobody reads it; the question this answers — "does the agent still work with a model that has opinions?" — matters at the moment something is about to be published, so release.yml `needs:` it. It also runs on PRs that touch the dependency manifests, which is where upstream drift actually arrives. The test asserts mechanics only: the loop reached a final answer, the model drove the MCP tool, and the arguments parsed into the declared schema. Asserting wording or plan shape would be asserting the model's judgement, which changes with every weight and sampler — that is how a suite ends up disabled. Sampling is greedy with a fixed seed so a failure means the agent broke rather than the dice rolled differently, and the step retries once so a single stray run is not treated as signal. The job is advisory (continue-on-error) rather than blocking, and the reason is written into the workflow: I could verify the test file against a live endpoint, but not the tool-calling half. Qwen2.5-1.5B served by llama-cpp-python's chatml-function-calling shim emitted no tool calls at all — not through the agent, not through a hand-rolled request with one trivial tool. That shim is not what CI runs (llama-server --jinja uses the model's own template, which declares tool support), so the result does not transfer; but nobody has yet watched this exact combination drive a tool end to end, and blocking a publish on an unproven assertion is the wrong default. Flip one line once it has been green for a few releases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011FKop4At26QqqkwVGEjJur
The 1.5B never drove the tool: it answered from imagination, once reporting a tool's own NAME as the secret. Measured on the same runtime (llama-server --jinja, greedy), a Qwen2.5-3B calls the tool and returns the real value, so the job now pins 3B and blocks the release instead of warning past it. - helpers/mcp-server: add a `secret` tool returning a per-run random value. A task that asks for it cannot be answered without the tool, which turns "did the model actually use it?" into a factual assertion. - live-model.test: ask a QUESTION rather than give an order — the planner's canned "Answer the user directly" step is attractive to small models and wins against imperative phrasing even on a 3B. Assert the secret appears in the final text, pinning the whole round trip through the tool result. - live-model.yml: pin a verified llama.cpp image tag, drop continue-on-error. - integration.test: tool-count expectations follow the new helper tool. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011FKop4At26QqqkwVGEjJur
This was referenced Aug 16, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why here, not nightly
A nightly run reports at 3am on yesterday's commit and nobody reads it. The question this layer answers — does the agent still work with a model that has opinions? — matters at the moment something is about to be published, so
release.ymlnowneeds:it. It also runs on PRs touchingpackage.json/package-lock.json, which is where upstream drift actually arrives (the autoupdate flow's PRs land there).Nightly's one unique property is catching drift with no commits at all; that is already covered by the autoupdate flow opening PRs.
What
tests/live-model.test.ts— the same loop as the scripted integration suite, driven by a real model behind any OpenAI-compatible endpoint. Skipped unlessAGENT_LIVE_MODEL_URLis set, sonpm testis unaffected (172 tests, 1 skipped locally)..github/workflows/live-model.yml— pulls a pinnedllama.cppserver image and a pinned GGUF (cached), serves it on the runner, runs the test. No hosted API, no key.tests/helpers/mcp-server.ts— asecrettool returning a per-run random value.release.ymlgains alive-modeljob thatreleasedepends on.Assertions are mechanics only: the loop reached a final answer, the plan parsed, the model drove the MCP tool, and the tool's result reached the final text. Asserting wording or plan shape would be asserting the model's judgement, which changes with every weight and sampler — that is how a suite ends up disabled. Sampling is greedy with a fixed seed, and the step retries once so one stray run isn't treated as signal.
It is a blocking gate, and the model floor is measured
An earlier revision of this PR shipped the job advisory (
continue-on-error: true) because the tool-calling half was unverified. It has since been run for real, and both the flag and the model are now decided by measurement rather than caution.What the runs showed, all on the runtime this workflow actually uses (
llama-server --jinja, greedy):files__secretzq-…valueSo the pin is 3B, and
continue-on-erroris gone. The earlier negative result for the 1.5B was partly my own instrumentation: the first attempt went throughllama-cpp-python'schatml-function-callingshim, and on the realllama-serverthe 1.5B does emit tool calls for a direct single-tool request. It still doesn't do it through the agent, which is what matters here.Two findings worth keeping, both written into comments next to the code they explain:
secrettool: a value only the server knows.Note on
release.ymlCLAUDE.md says not to touch that file unless explicitly asked. This change was explicitly requested (gate the release on a real-model run); the edit is two lines — a
live-modeljob that calls the reusable workflow, andneeds: live-modelon the publish job. The quirks section in CLAUDE.md records why, so a future session doesn't "fix" it away.typecheck,format:check,build,testare green (171 pass, 1 skipped), andlive-model.test.tspassed against the 3B in 10.3s.Generated by Claude Code