Agent Quality Kit
#Agent Quality Kit
An LLM agent harness with versioned prompts, runtime guardrails, automated evals and a CI quality gate — the full loop that makes agent behaviour predictable, measurable and safe to ship.
The example agent reviews code diffs and returns structured findings. The agent is the pretext; the platform around it is the project.
Runs offline, with no API key: the default provider is a deterministic mock, so
make evalworks on your laptop and in CI at zero cost and with zero variance. Switching to the real API is one environment variable.
#Why this exists
Calling an LLM is easy. Everything after that is the actual engineering problem:
| Question | Where this repo answers it |
|---|---|
| How do you stop an agent from looping, burning budget, or wrecking a repo? | src/aqk/agent.py, src/aqk/budget.py, src/aqk/guardrails.py |
| How do you know a prompt change made things better and not worse? | evals/runner.py, evals/scorers.py |
| How do you stop a regression from reaching the team? | evals/gate.py, .github/workflows/ai-quality.yml |
| What did it cost, and why? | src/aqk/pricing.py, src/aqk/telemetry.py |
| Can you reproduce yesterday's run exactly? | traces carrying prompt.version + prompt.hash + model + params |
| How do you test any of this without spending money? | src/aqk/provider.py (deterministic mock) + tests/ |
The thesis: the model is the only non-deterministic component in the system. Anything you can move out of the prompt and into the harness becomes a guarantee instead of a probability. What's left — judgement — is measured by evals and protected by gates.
Source code and identifiers are in English; inline comments are in Brazilian Portuguese, which is the author's working language.
#Running it
pip install pyyaml # the only dependency in mock mode make test # 57 deterministic harness tests (<1s, no API key) make run # run the agent on one diff: cost, tokens, trace make eval # full eval suite make eval PROMPT=1.0.0 # same suite, previous prompt version make gate # compare against the baseline, fail on regression make trace # summary of the last trace
Against the real API:
pip install anthropic export AQK_PROVIDER=anthropic ANTHROPIC_API_KEY=... make eval
#What the commands show
#make eval — stratified measurement, with uncertainty and cost
EVAL code_review@1.1.0 (hash f469c963aa5c) · model claude-opus-5 · provider mock
suite=full k=1 dataset=63fe5faadf9b
--------------------------------------------------------------------
overall pass rate 92.3% (12/13) CI95 [66.7%, 98.6%]
critical cases 100.0% (n=5) ← gate threshold: 100%
--------------------------------------------------------------------
by category:
correctness 100.0% (3/3) ████████████████████
false-positive 0.0% (0/1)
noise 100.0% (2/2) ████████████████████
performance 100.0% (2/2) ████████████████████
security 100.0% (5/5) ████████████████████
--------------------------------------------------------------------
total $0.0628 · mean/case $0.0048 · tokens 11,358 · cache hit 69.0%
Three deliberate decisions are visible in that report:
- A confidence interval next to the rate. With 13 cases the interval is wide — and saying so is the honest thing to do. It is what stops anyone from celebrating a 3-point "improvement" that fits entirely inside the noise.
- Per-category metrics, not just the average. A global average hides localized regressions; the gate looks slice by slice.
- The suite is deliberately not all green. Case
cr-013documents a known false positive in the detector (a parameterized query flagged as SQL injection). A suite that is always green measures nothing. The gate demands no regression, not perfection.
#make gate — a subjective standard turned into an objective, blocking check
Simulating a pull request that reverts the prompt to the previous version:
GATE code_review@1.0.0 vs baseline 1.1.0
--------------------------------------------------------------------
metric baseline current delta
overall pass rate 92.3% 38.5% ▼ 53.8%
critical 100.0% 0.0% ▼ 100.0%
security 100.0% 0.0% ▼ 100.0%
performance 100.0% 0.0% ▼ 100.0%
mean cost/case $0.0048 $0.0037 ▼ $0.0011
--------------------------------------------------------------------
❌ GATE BLOCKED
[critical_must_pass] critical cases failed: ['cr-001', ...]. Threshold is 100%, no margin.
[category_regression:security] security: 100.0% → 0.0% (cases: [...])
Note that cost went down and the gate blocked anyway. Cheaper and worse is not an optimization.
Gate rules (evals/gate.py):
| Rule | Type | Rationale |
|---|---|---|
critical cases at 100% |
absolute | security does not negotiate statistical margin |
| Overall pass rate must not drop | relative to baseline | "don't regress" ages better than "hit 92%" |
| No category drops more than 2pp | relative | the average hides localized regressions |
| Mean cost/case must not rise >20% | budget | quality without cost beside it is half the information |
| Dataset hash changed | warning, not a block | changing the ruler is legitimate, but needs human review |
#Architecture
┌──────────────── HARNESS (deterministic) ─────────────────┐
diff ──► IN-GUARD ─┤ context assembly → loop → budget → telemetry ├─► OUT-GUARD ──► findings
(secrets, │ ▲ │ │ (schema,
size) │ │ ▼ │ secrets,
│ │ ACTION-GUARD → tool │ grounding)
│ └── RESULT-GUARD ◄─┘ │
└──────────────────────────────────────────────────────────┘
│
prompts/ (versioned) │ traces/ (OTel GenAI attrs)
▼
evals/ ──► gate ──► CI blocks the merge
| Module | Responsibility | The idea it encodes |
|---|---|---|
agent.py |
the loop as a state machine | the terminal state (done, budget_exceeded, loop_detected, blocked, truncated) is the first partition of the cause space during an investigation |
budget.py |
tokens, cost, turns, wall-clock | a soft limit warns the model, a hard limit ends the run with a partial result — graceful degradation instead of dying silently |
guardrails.py |
the runtime control plane | failure policy (fail-closed / fail-open) declared explicitly per guardrail; new guardrails start in shadow mode |
tools.py |
tool contracts and sandbox | name and description are prompt, schema is contract; an error is an observation, not an exception |
prompts.py |
versioned registry | a prompt is versioned together with its model and parameters — without that, nothing reproduces |
provider.py |
model abstraction + mock | record/replay is what makes the harness testable in CI without an API key |
pricing.py |
per-call cost | cache reads (~10%) and writes (~1.25×) accounted for separately from fresh input |
telemetry.py |
JSONL traces, OTel GenAI style | every span carries model, prompt version, tokens (incl. cache), cost and terminal state |
#Engineering decisions (and their trade-offs)
A manual loop instead of a framework. The SDK ships a tool runner and that is the right way to start. The loop here is manual because a platform needs things the runner does not expose: its own budget accounting, guardrails in the middle of the loop, a compaction policy, record/replay for testing, and telemetry carrying the organization's own attributes. Buy first, build when the platform matures.
Context split into a stable prefix and volatile content. Rules and tool schemas live in system (cacheable); the diff, which changes per case, goes in the user message. Prompt caching is a prefix match — one different byte at the front invalidates everything after it. That is also why tool ordering is deterministic (tools.py), with a test enforcing it.
The input guardrail masks the secret's value but preserves the shape of the code. api_key = "prod_live_..." becomes api_key = "REDACTED_SECRET_VALUE". The secret never leaves the building, and downstream analysis can still see that a hardcoded credential is there. Masking in a way that destroys the syntax would blind the review.
Anything a tool returns is data, never instruction. It comes back wrapped in <dados_nao_confiaveis>, and if it contains an injection pattern the run drops privilege: mutating tools then require human approval. The defence does not rely on detecting the injection — it relies on leaving the attack with no channel to act through.
Tool errors come back as structured observations. file not found. Nearby files: [...] lets the model correct itself on the next turn. An exception would kill the agent. This is the cheapest large quality lever in agent work.
LLM-as-judge only for the residue. The ladder is: structural assertion → real execution (run_checks) → judge. The judge (evals/judge.py) uses a binary rubric, asks for evidence before the verdict, and declares its calibration (kappa=0.78 against 60 human labels). Below KAPPA_FLOOR = 0.60 it refuses to be used as a gate — an uncalibrated judge is automated opinion.
Template delimiter is {{@var}}, not {{var}}. The injected content is source code, and Blade/Handlebars/Jinja use {{ }}. The collision broke every review of a .blade.php file — a bug the eval suite itself surfaced.
#Statistics the runner uses
- Wilson interval instead of the normal approximation: better behaved at small n.
- McNemar's test (
scorers.mcnemar_p) for paired comparison between versions. Because both sides run on the same cases, only the discordant ones carry information: ~100 paired cases detect what would need ~900 per arm in an unpaired comparison. - k samples per case (
--k 3): the system is stochastic, so the criterion is a pass rate over k, not pass/fail from a single run.
#Layout
prompts/code_review/1.0.0.yaml prompt + model + params + changelog + owner
prompts/code_review/1.1.0.yaml version with security rules (the A/B in the demo)
prompts/judge/1.0.0.yaml binary judge rubric, calibration declared
src/aqk/ the harness
evals/dataset/code_review.yaml 13 cases with category, severity and PROVENANCE
evals/fixtures/ the diffs
evals/{runner,scorers,judge,gate}.py
evals/baseline.json the current ruler (promoted only in a reviewed PR)
tests/ 57 deterministic tests
.github/workflows/ai-quality.yml smoke on PRs, full on merge, plus a nightly cron
Every dataset case carries a provenance field — the incident that produced it. That is what stops someone deleting an "odd looking" case six months later. Every incident becomes a permanent case.
CI also runs on a cron, not only on pull requests: in LLM systems a good share of regressions come from outside your commit (a model update, a changed index, an altered tool).
#What this project deliberately is not
- It is not a good vulnerability detector.
tools.RULESis plain regex and it gets things wrong — casecr-013documents exactly that. The object of study is the quality pipeline, not state-of-the-art static analysis. - There is no RAG. The context fits in the window here; adding retrieval would be complexity without a problem.
- Numbers in mock mode are simulated. Token counts are estimated; the path the cost travels (per call → trace → gate) is the production one.
- No session persistence or resume. The loop is short by the nature of the task.
#License
MIT