Evaluating it on your traces
Before you put the guardrail in front of your tools, run your agent's past calls through it: how many it would have stopped, by which rule, how often it would have asked, and, where you label the calls, how often it was wrong. Everything is read on your machine, and reports hold tool names, rule names, counts and decision ids, never an argument value or a word a person said.
Trace formats
| Format | What it is |
|---|---|
jsonl |
one call a line: {"conversation", "tool", "arguments", "turns"?, "result"?, "label"?}; or one turn a line: {"conversation", "turn"}, which the calls after it read |
openai |
chat logs: a conversation a line (or a JSON list), each its messages with tool_calls and tool replies |
anthropic |
the same with tool_use and tool_result blocks |
langsmith |
an export of runs: the tool runs (run_type: tool) of each trace, in time order |
tau2 |
a tau2-bench results file: each simulation a conversation |
auto tells them apart by the first record. Each call is checked with the person's turns said before it and what
tools returned before it, and each conversation is its own session.
Replaying: what would it have done?
replay(trace) in stalwart mcp, or evaluation.replay in Python:
- each call's answer, and the counts allowed, asked about and refused, by rule;
- the calls that would have been stopped (conversation, position, tool, rule and decision id);
- confirmations per 100 calls: how often your people would be asked;
- the p50 and p95 decision time;
deterministic: a second run from a fresh guardrail gave identical decision ids.
Evaluating: was it right?
Label each call should_allow, should_confirm or should_refuse (a label on each line), and evaluate(labelled):
| answered allow | answered confirm | answered refuse | |
|---|---|---|---|
| should allow | correct | false alarm | false alarm |
| should confirm | miss | correct | false alarm |
| should refuse | miss | miss | correct |
- A false alarm is a call that should run, stopped or asked about: it costs your people time.
- A miss is a call that should be stopped, let through (or only asked about where it should be refused): it is the harm the guardrail is there to stop. A miss usually means a rule you have not written yet.
The report gives the matrix, the counts and the calls behind each false alarm and miss, overall and per rule.
Counterfactual: what would it have changed?
counterfactual(trace, "check.py:outcome") scores each conversation with your own outcome check, as run and with
the calls the guardrail stopped removed. Your function is given a conversation's id and the calls that ran, and
returns a number or a truth value (for example, whether the database it leaves is the one the task expected).
This is an offline counterfactual: the rest of the conversation is kept as recorded. A live agent that met the refusal may have gone on differently.
Your Guardrails AI guard, with and without ours
compare_with_gai(trace, "guard.py:guard") runs every call through your Guardrails AI guard alone, and through it with
our validator added: how many each lets through, and the calls only ours stops. It needs stalwart-agents[guardrails]
where the server runs.
Holes in your rules
hunt("rules.yaml") runs the bug hunter on your rule sheet: each finding is a way through that your rules allow, with
a witness to replay. redteam(server) proves an MCP server's tools behind the gateway for any call a model may make.
Both run on the service, with your key. They are a quality check of your own rules, not a certificate.
The tau2-bench benchmark
benchmark("tau2-airline") and benchmark("tau2-retail") fetch the third-party tau2-bench data from its pinned
upstream commits into benchmarks/, on demand; nothing is bundled. The published figures (airline: 114 of 1,161 writes
blocked; retail: 290 of 3,450) come from Stalwart's own replay of that data, which the package does not yet carry:
the report gives them as published, says they were not reproduced here, and names the sources it fetched.