Response Verification¶
What It Actually Checks¶
When a tool runs, it produces a response. Response verification checks whether that response looks normal based on rules you define — catching integration bugs, schema drift, and misconfigured test stubs before they silently corrupt your agent's reasoning.
Specifically, it flags responses that:
- Arrived impossibly fast (< 2ms) — indicating no real I/O happened (e.g. a mock left in production, a cache misconfiguration)
- Are missing fields you declared as required — indicating API schema changes or incomplete responses
- Don't match patterns you expect in valid responses — indicating error payloads being returned as success
- Contain statistically anomalous numeric values — indicating unit changes, corrupted data, or unexpected API changes
What it does not do: It cannot detect whether an LLM fabricated a fact in its text response. It only inspects the actual return value of your Python function.
How It Works¶
The verifier combines up to four independent signals into a weighted confidence score:
Signal 1: Execution Time¶
Real I/O takes time. A tool that wraps a network call, database query, or file operation will never legitimately complete in under 2ms. If it does, something structural is wrong — a stub wasn't removed, a cache is short-circuiting the real call, or the function never reached the external system.
Timing thresholds:
< 2ms → almost certainly no I/O occurred (score: high)
< min_ms → below expected range (score: moderate)
≥ min_ms → within expected range (score: 0)
> 3σ → statistically anomalous vs historical baseline (score: moderate)
You define expected latency per tool:
Signal 2: Required Fields¶
Declare which keys must appear in a valid response. Missing any of them flags the response.
If humidity is missing from the response, that's a signal — whether because of an API change, a partial response, or an error body returned with a 200 status.
Signal 3: Pattern Matching¶
Register regex patterns that valid responses must match. At least one must match, or the signal fires.
verifier.register_tool(
"get_weather",
response_patterns=[
r'"temperature":\s*-?\d+(\.\d+)?', # temperature must be a number
r'"conditions":\s*"[^"]+"', # conditions must be a string
],
)
Signal 4: Statistical Plausibility¶
Once the verifier has accumulated enough historical values for a numeric field (≥ 5 samples), it flags values that fall outside mean ± 3σ.
historical temperature readings: [18, 21, 19, 17, 22, 20]
incoming value: 950
→ flagged as anomalous (outside mean ± 3σ)
Confidence Scoring¶
The four signals are combined with configurable weights into a single confidence score (0 = looks normal, 1 = definitely anomalous):
confidence = (
timing_weight × timing_score +
schema_weight × schema_score +
pattern_weight × pattern_score +
value_weight × value_score
) / sum(weights)
Default weights: timing=0.3, schema=0.3, patterns=0.2, values=0.2.
Basic Usage¶
Standalone verifier¶
from agentguard import ResponseVerifier
verifier = ResponseVerifier(threshold=0.6)
verifier.register_tool(
"get_weather",
expected_latency_ms=(100, 5000),
required_fields=["temperature", "humidity", "conditions"],
response_patterns=[r'"temperature":\s*-?\d+'],
)
result = verifier.verify(
tool_name="get_weather",
execution_time_ms=0.3,
response={"temperature": 72, "conditions": "sunny"},
)
print(result.is_anomalous) # True — missing "humidity", sub-ms timing
print(result.confidence) # 0.87
print(result.reason) # "Execution time 0.30ms is below the 2ms minimum..."
print(result.signals) # raw signal values for debugging
With @guard¶
from agentguard import guard
@guard(verify_response=True)
def get_stock_price(ticker: str) -> dict:
return requests.get(f"https://api.exchange.com/v1/{ticker}").json()
# Register the expected response profile on the guarded function
get_stock_price.register_response_profile(
expected_latency_ms=(50, 3000),
required_fields=["ticker", "price", "currency"],
response_patterns=[r'"price":\s*\d+(\.\d+)?'],
)
When a response is flagged as anomalous, AnomalousResponseError is raised:
from agentguard import AnomalousResponseError
try:
result = get_stock_price("AAPL")
except AnomalousResponseError as e:
print(f"Anomalous response (confidence: {e.result.confidence:.2f})")
print(f"Reason: {e.result.reason}")
Registering Tool Profiles¶
For accurate detection, define expected characteristics per tool:
from agentguard import ResponseVerifier
verifier = ResponseVerifier(threshold=0.7)
# External API — network latency expected
verifier.register_tool(
"get_weather",
expected_latency_ms=(150, 8000),
required_fields=["temperature", "humidity", "wind_speed"],
response_patterns=[
r'"temperature":\s*-?\d+',
r'"humidity":\s*\d+',
],
)
# Database query — fast, structured results
verifier.register_tool(
"query_users",
expected_latency_ms=(5, 2000),
required_fields=["id", "email", "created_at"],
response_patterns=[
r'"id":\s*\d+',
r'"email":\s*"[^@]+@[^"]+\.[^"]+"',
],
)
# Local file read — can be very fast, content varies
verifier.register_tool(
"read_file",
expected_latency_ms=(0.5, 500), # filesystem can be sub-ms
required_fields=[],
response_patterns=[],
)
Threshold Tuning¶
Three built-in levels:
Conservative (fewer false positives)¶
Use when legitimate tool responses vary significantly, or when blocking a real call is worse than letting an anomaly through.
Balanced (recommended default)¶
Use for general-purpose agents with mixed tool types.
Strict (catches more anomalies, more false positives)¶
Use when tool results feed critical decisions (financial transactions, access control, medical data).
Customising Signal Weights¶
verifier = ResponseVerifier(
threshold=0.6,
weights={
"timing": 0.5, # Timing is your most reliable signal
"schema": 0.3,
"patterns": 0.1,
"semantic": 0.1,
}
)
Interpreting Results¶
result = verifier.verify(
tool_name="get_weather",
execution_time_ms=2.1,
response={"temperature": 72, "humidity": 60},
)
print(result.is_anomalous) # True / False
print(result.confidence) # 0.0–1.0
print(result.reason) # human-readable explanation
print(result.signals)
# {
# "timing_score": 0.0,
# "schema_score": 0.33, # 1 of 3 required fields missing
# "pattern_score": 0.5,
# "semantic_score": 0.0,
# "timing_ms": 2.1,
# "expected_latency": (150, 8000),
# }
Troubleshooting¶
Many false positives (real responses flagged as anomalous)¶
- Widen
expected_latency_ms— your tool's real latency may vary more than expected. - Some tools use in-memory caches that return in < 2ms legitimately. Register a looser timing range:
- Increase the threshold:
ResponseVerifier(threshold=0.8).
Anomalies not being caught¶
- Lower the threshold:
ResponseVerifier(threshold=0.4). - Add
response_patternsto catch responses that look structurally wrong. - Add
required_fieldsfor fields that always appear in valid responses.
Consistent false positives on one tool¶
Override that tool's profile with a looser configuration rather than raising the global threshold.