$ teds read --post deterministic-state-evaluations-ai-agent-tool-calls
Evaluating Agentic Tool Calls and Environment Deltas: The Deterministic Testing Layer
How to test multi-turn AI agents by intercepting tool schemas, snapshotting environment state, and catching parameter hallucination before it touches production infrastructure.
This is Part 2 of a 5-part series on evaluating and hardening production AI agents. See also: Part 1: Moving Beyond LLM-as-a-Judge, Part 3: Statistical Rigor in AI Evals, Part 4: Curating Golden Trajectories, and Part 5: Building an Agent CI/CD Pipeline.
In Part 1, we covered the mathematical decay of multi-step agent trajectories and why evaluating raw text with LLM-as-a-judge leaves production systems blind to silent failures.
The foundation of any defensible evaluation framework is deterministic testing.
Before running statistical benchmarks or subjective quality checks, an agent must prove it can interact with software systems predictably. That means validating three core mechanisms:
- Tool-Call Schema Conformance: Generating arguments that strictly satisfy API contracts.
- Environment State Deltas: Mutating external systems (databases, filesystems, APIs) to match the required goal state.
- Execution Efficiency & Loop Prevention: Halting cleanly when blocked without thrashing or exceeding compute budgets.
Here is how to build an inspectable deterministic evaluation layer for autonomous agents.
1. Schema Conformance: Catching Parameter Hallucination
When an agent invokes a tool, the foundation model outputs a structured JSON payload representing the tool name and argument dictionary.
In production, models fail tool calling in subtle ways that simple JSON parsing often misses:
- Type Coercion Failures: Passing a string
"100"where an integer100is required. - Enum Drift: Inventing plausible-sounding enum values (e.g., passing
"status": "in_progress"when the schema strictly specifies["pending", "processing", "completed"]). - Required Key Omission: Silently dropping optional-looking fields that downstream APIs strictly require.
- Extra Key Infiltration: Injecting fabricated keys from the prompt context into the tool payload.
The Strict Schema Evaluator
Instead of relying on permissive parsers, run strict runtime validation on every tool call using Pydantic or JSON Schema:
from pydantic import BaseModel, Field, ValidationError
from typing import Literal, Optional
from enum import Enum
class RefundReason(str, Enum):
FRAUD = "fraudulent"
CUSTOMER_REQUEST = "customer_requested"
DEFECTIVE = "item_defective"
class ProcessRefundSchema(BaseModel):
transaction_id: str = Field(pattern=r"^tx_[a-zA-Z0-9]{12}$")
amount_cents: int = Field(gt=0, le=1000000)
reason: RefundReason
notify_customer: bool = True
idempotency_key: str = Field(min_length=16)
class Config:
extra = "forbid" # Strictly reject hallucinated parameters
def evaluate_tool_call(tool_name: str, arguments: dict) -> tuple[bool, Optional[str]]:
tool_registry = {"process_refund": ProcessRefundSchema}
if tool_name not in tool_registry:
return False, f"Unknown tool invocation: {tool_name}"
schema = tool_registry[tool_name]
try:
schema.model_validate(arguments)
return True, None
except ValidationError as e:
return False, e.json()
When an agent evaluation fails at this layer, you receive an immediate, actionable schema diff. You know exactly which field failed validation without spending inference tokens on a secondary LLM judge.
2. Environment Delta Testing: Asserting State Mutations
The most critical test of an agent is not what it generated in its final message—it is what changed in the environment.
To test state mutations reproducibly:
- Snapshot: Capture a snapshot of the mock environment before the agent begins.
- Execute: Allow the agent to run within an isolated sandbox (e.g., a temporary SQLite database, a temporary directory, or a mocked HTTP client).
- Diff: Compare the final environment state against the pre-condition snapshot to compute an exact state delta.
- Assert: Validate that only intended mutations occurred and no unintended state leaks happened.
Concrete Implementation: Database Delta Harness
import sqlite3
from typing import Any, Dict
def snapshot_table(conn: sqlite3.Connection, table: str) -> Dict[Any, Dict[str, Any]]:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM {table}")
return {row["id"]: dict(row) for row in cursor.fetchall()}
def assert_table_delta(before: Dict, after: Dict, expected_mutations: Dict):
"""
Assert that the only rows modified, created, or deleted match the expected plan.
"""
modified = {}
for row_id, after_row in after.items():
if row_id not in before:
modified[row_id] = {"action": "inserted", "data": after_row}
elif before[row_id] != after_row:
modified[row_id] = {"action": "updated", "before": before[row_id], "after": after_row}
for row_id in before:
if row_id not in after:
modified[row_id] = {"action": "deleted", "data": before[row_id]}
assert modified == expected_mutations, (
f"Unexpected state mutation!\nExpected: {expected_mutations}\nActual: {modified}"
)
By asserting state deltas:
- If the agent hallucinated that it updated an account but failed to issue the query, the delta test fails.
- If the agent updated the target record but accidentally deleted adjacent records, the delta test immediately catches the regression.
3. Detecting Infinite Loops and Thrashing
When tools return error codes (e.g., 404 Not Found or 429 Rate Limit), poorly bounded agents frequently enter thrashing loops: they retry the identical tool invocation with identical parameters turn after turn.
A robust deterministic evaluation suite monitors trajectory efficiency:
The Trajectory Guard Metrics
- Tool Invocation Entropy: Is the agent trying diverse strategies or repeating failed inputs?
- Step Budget Ceiling: Rejecting trajectories exceeding a pre-set step limit ($k_{\text{max}}$).
- Token / Cost Budget: Bounding maximum cumulative context length and API cost per run.
def assert_trajectory_healthy(trajectory_steps: list, max_identical_calls: int = 2):
seen_calls = {}
for step in trajectory_steps:
call_signature = (step.tool_name, frozenset(step.arguments.items()))
seen_calls[call_signature] = seen_calls.get(call_signature, 0) + 1
if seen_calls[call_signature] > max_identical_calls:
raise AssertionError(
f"Agent entered thrashing loop on tool '{step.tool_name}' "
f"with arguments {step.arguments}. Repeated {seen_calls[call_signature]} times."
)
Putting It Together in CI
Deterministic evaluations run in standard test runners (such as pytest). They require:
- No external model API calls for grading
- No internet access (fully mocked sandboxes)
- Millisecond execution speed
By gating pull requests on these three deterministic layers, engineering teams guarantee that any prompt modification, fine-tuned checkpoint, or framework upgrade preserves tool calling and environment safety.
The Next Step
Once your deterministic layer is running, how many test runs do you actually need to determine whether an agent is reliable?
In Part 3, we break down The Actuarial Approach to AI Evals: why running 20 tests gives you almost zero statistical confidence, and how to compute Wilson score confidence intervals to measure real performance.
Need Help Architecting Agent Evaluation Systems?
Building isolated sandboxes, state assertions, and mock harnesses for complex agents requires senior systems engineering.
We work with AI startups and devtool companies as a Hacker-in-Residence to build clean, reproducible evaluation architectures that bridge the gap between prototypes and production.
- Read about our AI Engineering & Consulting Services
- Check our Model Red-Teaming Practice
- Review our technical work and deep dives on the TEDS Field Notes