You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: polish README — add Cost Assertions section, fold Quick Start subtitle, unify H3 title case
Second pass on the README after the Real-World Examples reshuffle.
Surface-level cleanups and one small section addition, driven by a
top-to-bottom re-read of the file.
Structural:
- Drop the "Why agentverify?" H2 and fold its body directly under
the tagline so the opening flows as one continuous pitch, then
add a "Who is this for?" H2 with five concrete reader-pain
bullets before Install.
- Add a dedicated "Cost Assertions" subsection under Assertion
Modes, placed before "Latency Assertions" to match the order
used in Quick Start, the `assert_all` example, and the Error
Messages samples.
- Fold "— No LLM Required" out of the Quick Start heading into
its opening sentence.
- Fold "— Test Routing Without an LLM" out of the Tool Mocking
heading into its opening sentence.
- Rename "Request Matching (Stale Cassette Detection)" to
"Stale Cassette Detection" and rephrase its opener to lead
with *why* the check exists rather than *what* it does.
- Rename the H3 "How do cassettes age in practice?" to a
declarative "Keeping Cassettes Current".
- Unify all H3 headings to title case
("Assert Step-to-Step Data Flow", "Workflow-Style Agents with
`step_probe`", "When to Use MockLLM vs Cassettes").
Copy:
- Rewrite the "Near-term:" roadmap bullets with a one-line mission
statement that keeps the ambition but drops the bolder
"pytest for Python" comparison.
- Rewrite "Who is this for?" to stay punchy without venting at
the reader ("looks fine locally" is now an "informal signoff",
not a Slack dig).
- Soften the "walks the multi-line feedback into..." phrasing in
the OpenAI Agents example — `assert_step_uses_result_from` now
*finds* the feedback *inside* the next step.
- Replace "Legacy v0.2.0 form" for flat `tool_calls` in the
`ExecutionResult.from_dict` table with "Shortcut for
single-step cases", which matches how the Quick Start actually
uses it.
Fixes:
- Update the anchor `#quick-start--no-llm-required` → `#quick-start`
after the heading rename.
- Replace the `...` placeholder in the `step_probe` test sample
with an executable `assert_step_output(..., contains="42")`.
- Swap the MATCHES regex example for email and event-id patterns so
it doesn't duplicate the Strands example's `/points/` URL shape.
- Contextualize the `list_issues` / `get_issue` walkthrough in
Step-Level Assertions as a separate example and link to the
LangChain GitHub example.
- Reflow "Using a real agent framework?" to point at both the
Real-World Examples section and the Examples table, and drop
the duplicated "walk through" wording.
- Bump the CI Integration snippet to Python 3.14 (matching the CI
matrix) and add a comment noting that `pip install "agentverify[all]"`
is shown separately for clarity; in practice users add it to
their `requirements.txt`.
- Reorder Error Messages so CostBudgetError appears before
LatencyBudgetError, matching the assertion order used elsewhere
in the README.
- Normalize US English (`serialization`, `neighborhood`).
Copy file name to clipboardExpand all lines: README.md
+78-35Lines changed: 78 additions & 35 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -10,28 +10,34 @@
10
10
11
11
agentverify is a pytest plugin for deterministic testing of AI agent actions. Record real LLM calls once, replay them in CI with zero cost, and assert exactly what your agent did — which tools it called, what data flowed between steps, how much it cost, and whether it stayed within your safety rules.
12
12
13
-
## Why agentverify?
14
-
15
13
Prompt engineering and eval frameworks tell you whether an LLM said the right thing. Production agents fail for a different set of reasons: they call the wrong tool, skip a step, hallucinate a parameter that was supposed to come from the previous tool's output, or quietly burn through your token budget. Those are deterministic bugs, and they deserve deterministic tests.
16
14
17
-
agentverify records real LLM SDK calls once and replays them in CI with zero cost. On the recording you can assert:
15
+
On a recording you can assert:
18
16
19
17
-**Tool call sequences** — exact order, subsequence, or set membership, with regex / wildcard argument matching
20
18
-**Step-level execution** — each LLM call is a step; assert what tool was called at step N, what the step's output was, and that step N's input references data produced by step M
21
19
-**Budgets** — token counts, cost in USD, end-to-end latency
22
20
-**Safety** — a list of tools that must never be called, no matter what the LLM decides
23
21
24
-
It ships with built-in adapters for LangChain, LangGraph, Strands Agents, and OpenAI Agents SDK, and supports OpenAI, Amazon Bedrock, Google Gemini, Anthropic, and LiteLLM as LLM providers. Cassettes are human-readable YAML — commit them to git, review in PRs, run in CI without an API key.
22
+
Built-in adapters cover LangChain, LangGraph, Strands Agents, and OpenAI Agents SDK. LLM providers supported: OpenAI, Amazon Bedrock, Google Gemini, Anthropic, and LiteLLM. Cassettes are human-readable YAML — commit them to git, review in PRs, run in CI without an API key.
23
+
24
+
## Who is this for?
25
+
26
+
- You've shipped an agent with LangChain / LangGraph / Strands / OpenAI Agents SDK and need CI-safe regression tests for it
27
+
- Your agent calls multiple tools and you've hit bugs like "it ignored the tool result" or "it called the wrong tool"
28
+
- You want prompt and tool changes reviewed in PRs via a concrete diff, not an informal "looks fine locally" signoff
29
+
- You're building on MCP servers and need to pin down which tools get called with which arguments
30
+
- You want token, cost, and latency budgets enforced in CI, not caught only on the billing dashboard
25
31
26
32
## Install
27
33
28
34
```bash
29
35
pip install agentverify
30
36
```
31
37
32
-
## Quick Start — No LLM Required
38
+
## Quick Start
33
39
34
-
Copy this into `test_agent.py` and run `pytest`. No API keys, no cassettes — just pure assertions.
40
+
Copy this into `test_agent.py` and run `pytest`. No LLM calls, no API keys, no cassettes needed — just pure assertions on an `ExecutionResult`.
35
41
36
42
```python
37
43
from agentverify import (
@@ -66,7 +72,7 @@ def test_output():
66
72
assert_final_output(result, contains="Tokyo")
67
73
```
68
74
69
-
> **Using a real agent framework?**The [Real-World Examples](#real-world-examples) below show how to test actual agents with built-in adapters for LangChain, LangGraph, Strands Agents, and OpenAI Agents SDK.
75
+
> **Using a real agent framework?**Walk through the [Real-World Examples](#real-world-examples) below for end-to-end tests with cassettes, or jump straight to the [Examples](#examples) table for the full list of runnable samples.
`assert_step_uses_result_from`walks the multi-line evaluator feedback into the next step's user message even though JSON serialisation would escape the newlines; the match succeeds across that boundary. Because each `Runner.run` call is recorded individually, the example builds the `ExecutionResult` straight off the cassette recorder — `from_openai_agents` is the right adapter for single-run agents, but the cassette layer is the natural single source of truth when the workflow spans several runs.
183
+
`assert_step_uses_result_from`finds the multi-line evaluator feedback inside the next step's user message even when JSON serialization would escape the newlines; the match succeeds across that boundary. Because each `Runner.run` call is recorded individually, the example builds the `ExecutionResult` straight off the cassette recorder — `from_openai_agents` is the right adapter for single-run agents, but the cassette layer is the natural single source of truth when the workflow spans several runs.
178
184
179
185
See [`examples/openai-agents-llm-as-a-judge/`](examples/openai-agents-llm-as-a-judge/) for the full loop, the cassette, and the remaining flat + step-level assertions (budget, safety, pass-verdict reached).
180
186
@@ -191,7 +197,7 @@ Each example ships with its own pre-recorded cassette, a detailed `README.md`, a
191
197
192
198
Every agentverify assertion takes an `ExecutionResult`. You can build one three ways:
193
199
194
-
1.**From a dict** — convenient for quick tests and fixtures, as in the [Quick Start](#quick-start--no-llm-required).
200
+
1.**From a dict** — convenient for quick tests and fixtures, as in the [Quick Start](#quick-start).
195
201
2.**From a built-in adapter** — one-liner for LangChain, LangGraph, Strands Agents, OpenAI Agents SDK. See [Framework Integration](#framework-integration).
196
202
3.**From a custom converter** — for other frameworks, map your output to the schema below. See [`examples/langchain-issue-triage/converter.py`](examples/langchain-issue-triage/converter.py) for a ~50-line reference implementation.
197
203
@@ -200,7 +206,7 @@ Every agentverify assertion takes an `ExecutionResult`. You can build one three
200
206
| Key | Type | Description |
201
207
|---|---|---|
202
208
|`steps`|`list[dict]`| Each step dict has `index` (int), `source` (`"llm"` / `"probe"` / `"tool"`), `tool_calls` (list), `tool_results` (list), `output` (str or None), `input_context` (dict or None), `name` (str or None). See [Step-Level Assertions](#step-level-assertions)|
203
-
|`tool_calls`|`list[dict]`|**Legacy v0.2.0 form** — when `steps` is absent, a flat `tool_calls` list is wrapped into a single synthetic step. Each dict has `name` (str, required), `arguments` (dict, optional), `result` (any, optional) |
209
+
|`tool_calls`|`list[dict]`|**Shortcut for single-step cases** — when `steps` is absent, a flat `tool_calls` list is wrapped into a single synthetic step. Used throughout the [Quick Start](#quick-start). Each dict has `name` (str, required), `arguments` (dict, optional), `result` (any, optional) |
204
210
|`token_usage`|`dict` or `None`|`{"input_tokens": int, "output_tokens": int}`|
205
211
|`total_cost_usd`|`float` or `None`| Total cost in USD (must be set manually — not auto-calculated from tokens or populated from cassettes) |
206
212
|`duration_ms`|`float` or `None`| Wall-clock duration in milliseconds (auto-populated by the `cassette` fixture and `MockLLM`) |
@@ -253,9 +259,9 @@ Cassettes are human-readable YAML (or JSON). Commit them to git, review in PRs.
253
259
|`RECORD`| Always call real LLM API and save to cassette file. |
254
260
|`REPLAY`| Always replay from cassette file. Raises error if file is missing. |
255
261
256
-
### Request Matching (Stale Cassette Detection)
262
+
### Stale Cassette Detection
257
263
258
-
Cassette replay verifies request content by default. Each replay request is compared against the recorded request:
264
+
Cassette replay verifies request content by default so a stale cassette fails loudly instead of returning the old response. Each replay request is compared against the recorded request:
259
265
-**Model name**: must match exactly (empty model in either side skips the check)
260
266
-**Tool names**: the sorted list of tool names must match (empty tools in either side skips the check)
261
267
@@ -274,6 +280,15 @@ with cassette("my_test.yaml", provider="openai", match_requests=False) as rec:
274
280
run_my_agent("What's the weather?")
275
281
```
276
282
283
+
### Keeping Cassettes Current
284
+
285
+
Cassettes drift when your prompts, tool schemas, or model versions change. agentverify's stale detection catches model-name and tool-name mismatches automatically (see [Stale Cassette Detection](#stale-cassette-detection)); a prompt-only change won't invalidate the cassette unless the model's tool-call output differs. In practice:
286
+
287
+
- Re-record on major prompt rewrites or tool schema changes
288
+
- Pin model versions (`gpt-4o-2024-08-06`, not `gpt-4o`) in your agent code so cassettes don't silently shift underneath you
289
+
- Treat cassette diffs as part of the PR review — a YAML diff next to the prompt diff is a feature, not noise
290
+
- Use `MATCHES` and `partial_args=True` to assert on the stable parts of tool arguments, so small wording changes in LLM outputs don't cascade into test failures
291
+
277
292
### Cassette Sanitization
278
293
279
294
Cassette files are sanitized by default when recording. API keys, tokens, and other sensitive data are automatically redacted before saving to disk.
`assert_cost()` enforces token and dollar budgets on an agent run. Either bound is optional — pass `max_tokens` only, `max_cost_usd` only, or both together:
`result.token_usage` is populated automatically by the `cassette` fixture, `MockLLM`, and every built-in framework adapter. `result.total_cost_usd` must be set manually (see [Build an ExecutionResult](#build-an-executionresult)) — agentverify does not compute dollar costs from tokens, since the pricing table would need per-model maintenance and get stale quickly.
381
+
348
382
### Latency Assertions
349
383
350
384
`assert_latency()` enforces a response time SLA on your agent. When you use the `cassette` fixture, wall-clock duration is captured automatically on context exit and exposed as `ExecutionResult.duration_ms`:
@@ -399,15 +433,15 @@ For agents that make multiple LLM calls per execution (ReAct, Plan-and-Execute,
399
433
400
434
`ExecutionResult.steps` is the single source of truth; `result.tool_calls` remains as a derived flat view for simpler cases.
401
435
402
-
> The [Real-World Examples](#real-world-examples) above show `assert_step` and `assert_step_uses_result_from` in action on Strands and LangGraph. This section covers the full API.
436
+
> The [Real-World Examples](#real-world-examples) above show `assert_step` and `assert_step_uses_result_from` in action on Strands and the OpenAI Agents SDK, with the LangGraph and LangChain examples linked below them. This section covers the full API.
403
437
404
438
```python
405
439
from agentverify import assert_step, assert_step_output, ToolCall, MATCHES
406
440
407
441
@pytest.mark.agentverify
408
442
deftest_plan_and_execute(cassette):
409
443
with cassette("trip_planner.yaml", provider="openai") as rec:
The check searches for any produced value (step M's `tool_results`, `tool_calls[*].result`, or `output`) inside step N's `input_context` or `tool_calls[*].arguments`. Strings are substring-matched; primitives (numbers, booleans) are matched structurally inside containers; JSON-encoded tool results are descended automatically.
438
472
439
-
Concretely, if step 0's `list_issues` tool returned `[{"number": 1}, {"number": 2}]` and step 1 called `get_issue(issue_number=2)`, the check finds that the number `2` produced by step 0 shows up in step 1's arguments — data flows correctly.
473
+
To see this concretely with a different example: if step 0's `list_issues` tool returned `[{"number": 1}, {"number": 2}]` and step 1 called `get_issue(issue_number=2)`, the check finds that the number `2` produced by step 0 shows up in step 1's arguments — data flows correctly. This is exactly the pattern the [LangChain GitHub issue triage example](examples/langchain-issue-triage/) verifies.
440
474
441
475
**Works on cassette replay.** Cassette recorders don't record tool execution results directly, but agentverify backfills them from the next step's `input_context` so you can assert data flow without any special adapter setup.
442
476
443
-
### Workflow-style Agents with `step_probe`
477
+
### Workflow-Style Agents with `step_probe`
444
478
445
479
Many production agents aren't pure ReAct loops — they mix LLM calls with caching, state management, validation, and conditional branches. `step_probe()` lets you mark logical step boundaries in your agent code so tests can assert on those non-LLM steps.
446
480
@@ -467,7 +501,7 @@ def test_cache_miss_path():
467
501
run_agent("what's the answer?")
468
502
result = rec.to_execution_result()
469
503
assert_step(result, name="fetch_cache", expected_tools=[]) # probe with no tool calls
470
-
assert_step(result, name="call_llm", ...)
504
+
assert_step_output(result, name="call_llm", contains="42") # the LLM step returned the answer
@@ -522,9 +560,9 @@ For other frameworks, build an `ExecutionResult` from your agent's output using
522
560
| LiteLLM |`pip install agentverify[litellm]`|
523
561
| All providers |`pip install agentverify[all]`|
524
562
525
-
## Tool Mocking — Test Routing Without an LLM
563
+
## Tool Mocking
526
564
527
-
`MockLLM` replays a list of predefined LLM responses that you define in code. No cassette file, no real API call. Useful when you want to test your agent's routing logic — does it call the right tools, in the right order, given a specific LLM response — without recording a cassette first.
565
+
`MockLLM` replays a list of predefined LLM responses that you define in code. Test your agent's routing logic without a cassette or a real LLM call — useful when you haven't recorded yet, or when you want to drive the agent through hypothetical responses (errors, edge cases) that would be awkward to elicit from a real model.
Provide one `mock_response(...)` per LLM call your agent is expected to make. If your agent makes more calls than you've queued, `MockLLM` raises `CassetteMissingRequestError` so under-specified tests fail loudly.
555
593
556
-
### When to use MockLLM vs cassettes
594
+
### When to Use MockLLM vs Cassettes
557
595
558
596
| Use MockLLM when | Use cassettes when |
559
597
|---|---|
@@ -579,7 +617,9 @@ jobs:
579
617
- uses: actions/checkout@v5
580
618
- uses: actions/setup-python@v6
581
619
with:
582
-
python-version: "3.12"
620
+
python-version: "3.14"
621
+
# Shown separately here for clarity — in practice, list agentverify
622
+
# alongside the rest of your project's deps in requirements.txt.
583
623
- run: pip install "agentverify[all]"
584
624
- run: pip install -r requirements.txt # your project's deps
Other error types follow the same pattern: `SafetyRuleViolationError`, `FinalOutputError`.
623
663
624
664
## Requirements
@@ -642,10 +682,13 @@ See each example's README for setup and recording mode details.
642
682
643
683
## Roadmap
644
684
685
+
We want agentverify to become the way people regression-test agents in CI — tests that live next to the agent code, in git, reviewed in PRs alongside the prompt and tool changes that affect them.
686
+
645
687
- Async support — first-class `asyncio` testing for async agents and tools
646
688
- Responses API cassette adapter — record/replay for OpenAI Agents SDK (Responses API) with end-to-end example
647
689
- Framework adapters for Google ADK and CrewAI — pending async support and stable tool-call APIs from these frameworks
648
690
- Cost estimation from tokens — auto-calculate `total_cost_usd` from token usage and model pricing
691
+
- Eval-framework bridges (exploring) — compose ragas / deepeval quality scores with agentverify's action assertions in a single test report
0 commit comments