Check agent behaviour¶
Most tests mix a few kinds of check in one chain:
import pytest
@pytest.mark.asyncio
async def test_books_flight_correctly(monocle_trace_asserter):
await monocle_trace_asserter.run_agent_async(
travel_agent, "google_adk", "Book a flight to Mumbai on 22 Nov")
monocle_trace_asserter.called_tool("book_flight").contains_input("Mumbai") # calls, inputs
monocle_trace_asserter.under_token_limit(2000) # limits
monocle_trace_asserter.with_evaluation("okahu") \
.check_eval("hallucination", expected="no_hallucination") # eval
Pick the right kind of check¶
| Type | Checks | Methods |
|---|---|---|
| Calls | The right tool or sub-agent was used, how often, nothing forbidden | called_tool, called_agent, does_not_call_*, count / min_count / max_count |
| Inputs and outputs | The arguments passed; what the answer contains or means | contains_input, contains_output, has_output with with_comparer(...) |
| Limits | Token and time budgets | under_token_limit, under_duration |
| Span details | Attributes, events, which turn something happened in | where, has_attribute, has_event, has_scope |
| Code-based checks | Keywords, patterns, valid JSON, exact match, similarity, computed locally | keyword_presence, regex_match, json_validity, exact_match, bert_score |
| Evals (LLM-as-a-judge) | What needs judgement: correct, grounded, relevant, safe, on-topic | check_eval |
The rule: if you can state the question exactly, use one of the first five. They are fast, cheap, and give the same answer every run. If it needs judgement, use an eval.
Calls, inputs, outputs, limits¶
@pytest.mark.asyncio
async def test_books_the_right_flight(monocle_trace_asserter):
await monocle_trace_asserter.run_agent_async(travel_agent, "google_adk", "Book a flight to Mumbai")
monocle_trace_asserter \
.called_tool("book_flight", agent_name="flight_booking_agent") \
.contains_input("Mumbai") \
.contains_output("booked")
@pytest.mark.asyncio
async def test_stays_within_limits(monocle_trace_asserter):
await monocle_trace_asserter.run_agent_async(travel_agent, "langgraph", "Summarize this document")
monocle_trace_asserter \
.called_agent("summarizer_agent") \
.under_token_limit(500) \
.under_duration(5.0, units="seconds", span_type="agent_invocation")
| What you want to check | Use |
|---|---|
| The right tool was called (and how often) | called_tool(name, agent_name=…, count= / min_count= / max_count=), called_any_tool(…) |
| The right sub-agent was used | called_agent(name, count=…), called_any_agent(…) |
| Something was not done | does_not_call_tool(name), does_not_call_agent(name) |
| The right arguments were passed | called_tool("book_flight").contains_input("Mumbai") |
| The answer contains / matches something | contains_output(…); for meaning, not exact text: with_comparer("similarity").has_output(…) |
| No wasted calls | called_tools(max_count=…), called_tool(name, max_count=…) |
| Token and time budgets | under_token_limit(n); under_duration(limit, units="ms"/"seconds"/"minutes", span_type=…) |
| Something happened in a given turn | called_tool("book_flight").has_scope("turn_id", "2") |
Selectors narrow what follows. called_agent("x").called_tool("y") checks only tool calls made
inside agent x.
Code-based checks¶
exact_match, regex_match, keyword_presence, json_validity, and bert_score run locally,
no model needed. Check that a reply mentions "flight" and never leaks a password or a stack trace:
{"span_type": "agentic.turn",
"eval": {"eval": "keyword_presence",
"eval_options": {"required_keywords": ["flight"],
"forbidden_keywords": ["password", "traceback"]},
"args": ["output"],
"expected_result": {"required_coverage": 1.0, "forbidden_absent": 1.0},
"comparer": "metric"}}
Evals (LLM-as-a-judge)¶
An eval is the check for what needs judgement: whether the answer is correct, grounded, relevant, safe, or on-topic. It is an LLM judge with a fixed rubric and a fixed set of labels. Two flavors, used the same way.
Built-in evals¶
| Area | Evals |
|---|---|
| Task and answer | correctness, answer_relevancy, argument_correctness, mcp_task_completion, conversation_completeness, knowledge_retention, summarization |
| Grounding (RAG) | hallucination, contextual_precision, contextual_recall, contextual_relevancy |
| Safety and behaviour | toxicity, bias, pii_leakage, misuse, offtopic, role_adherence |
| User experience | sentiment, frustration |
monocle_trace_asserter.with_evaluation("okahu") \
.check_eval("hallucination", expected="no_hallucination") \
.check_eval("bias", expected="unbiased") \
.check_eval("pii_leakage", not_expected="pii_leakage")
# A session-level eval, accepting either of two labels
monocle_trace_asserter.with_evaluation("okahu") \
.check_eval(fact_name="agentic_sessions", eval_name="role_adherence",
expected=["excellent_adherence", "good_adherence"])
If you narrow the trace first (for example called_agent("x")), the eval covers only that part.
Custom evals¶
A custom eval is your own template: a name, a judging prompt, and a set of labels, such as
refund_disclosure → disclosed / not_disclosed / not_applicable. Saved in Okahu, it is
versioned and available to every app in your account.
monocle_trace_asserter.with_evaluation("okahu") \
.check_eval("refund_disclosure", expected="disclosed") \
.check_eval(template_path="custom_eval_templates/refund_disclosure.json", expected="disclosed")
Reach for a custom eval only when no built-in fits and no exact check can decide the question.
Control what the agent faces¶
Mock tools¶
Test the agent's decisions without calling real systems, and simulate failures:
from monocle_test_tools import MockTool, ToolType
monocle_trace_asserter.with_mock_tool(MockTool(
name="adk_book_flight", type=ToolType.ADK,
response={"status": "success", "message": "Flight booked from {{from_airport}} to {{to_airport}}."}))
await monocle_trace_asserter.run_agent_async(
travel_agent, "google_adk", "Book a flight from San Francisco to Mumbai for 26 Nov 2025.")
monocle_trace_asserter.called_tool("adk_book_flight").contains_output("Flight booked")
{{arg}} in a response is filled with the tool's argument value. Set raise_error=True to make a
tool fail, then check that the agent retries sensibly or tells the user honestly instead of
claiming success.
Multi-turn tests¶
@MonocleValidator().monocle_multi_turn_testcase(multi_turn_cases)
async def test_multi_turn(multi_turn_case):
await MonocleValidator().test_multi_turn_agent_async(travel_agent, "google_adk", multi_turn_case)
Each case has a session_id and a list of turns. Use {previous_output} in a turn's input to
feed in the last answer. Memory carries across turns for ADK, LangGraph, Strands, LlamaIndex, and
Microsoft Agent Framework.
Generate tests with Claude Code¶
With the okahu@skills plugin, two skills write these tests for you:
monocle-test-genwrites a specific test you describe: the agent, the input, and what to check (tools, sub-agents, output, limits, or an eval). Point it at your test folder./ok-generate-tests ./my-appfinds your agents and tools and writes tests by category: routing, input and output validation, performance, quality (evals), and orchestration. It never overwrites existing files.
Use monocle-test-gen for the behaviours your plan calls out, and /ok-generate-tests for broad
coverage.
Next¶
Label, review & regress — record your own judgement, and turn every failure into a test the next version must pass.