Assertions

An assertion is one check over a finished run. Each entry under assert: is exactly one kind; a test may have any number. Deterministic assertions read the transcript, the responses, the tool calls, the metrics and the call timeline; judge and evaluator ask a model (see Judges). Every result carries a one-line reason that the console, junit.xml and result.json show, so a failure reads like a code review comment.

The transcript

AssertionPasses when
contains: textsome agent turn contains text (case-sensitive).
not_contains: textno agent turn contains text.
regex: patternsome agent turn matches the Go regular expression.
not_regex: patternno agent turn matches the Go regular expression (the matched text is the evidence when one does).
similar_to: "text"some agent turn means the same as text: the cosine similarity of their embeddings is at least min (0.8). similar_to: { text, min: 0.7, model } sets the threshold and the embedding model; the default model is models.embedder, or the usual embedder of your default model’s provider (Gemini text-embedding-004, OpenAI text-embedding-3-small). The closest turn and its score are the evidence either way, so a threshold is easy to tune. Use it where contains is too literal: the answer is right but worded its own way.
turns: { lte: 6 }the number of agent turns satisfies the one comparator (lt, lte, gt, gte, eq).

Responses (rest)

AssertionPasses when
status: 200every turn’s HTTP status is the code.
json_path: { path: $.status, equals: ok }the path exists in every turn’s response and, with equals, has that value (numbers and strings compared after JSON normalisation).

Tool calls

AssertionPasses when
tool_called: namethe agent called the tool at least once.
tool_called: { name: reschedule_booking, args: { day: saturday } }the agent called it with those argument values (a subset of the call’s arguments).
tool_called: { name: reschedule_booking, schema: { type: object, required: [day], properties: { day: { enum: [saturday, sunday] } } } }the agent called it, and the arguments of every call to it validate against the JSON Schema (draft 2020-12): a call the agent made with a wrong type, a missing field or a value outside an enum fails the assertion, with the first violation as the reason. args and schema combine. tamesu validate checks the schema itself.
tool_not_called: name (or with args)the agent never called it (with those arguments).
tool_sequence: { calls: [lookup_order, refund], exact: false }the agent called those tools in that order: as a subsequence of its calls, or exactly that list with exact.
delegated_to: { agent: billing, contains: "ORD-1001", regex: "..." }the agent sent a message to that remote agent (one of the suite’s remote_agents), and some message contained the text or matched the expression; a bare name checks the delegation only. See A2A: remote agents.
captured: { name: order_id, matches: "^ORD-" } (or equals)a value the test captured from the agent’s turns exists and matches; see capturing values.
golden: { file: golden/refund.json, mode: similar, min: 0.8 } (or turns: [...])the agent’s turns, in order, against reference turns: exact (normalised text), similar (word overlap of at least min, the default) or judge (the judge decides whether each turn means the same). A missing file is written from the run; tamesu run --update-golden rewrites them all.

Tool calls come from the target: the REST target’s response.tool_calls path, the LiveKit agent’s attributes and data messages.

Metrics

- metric: { name: turn_latency_ms_p95, lt: 4000 }
- metric: { name: time_to_first_audio_ms, lt: 1500 }

Exactly one comparator (lt, lte, gt, gte, eq). A metric the run did not collect fails with the list of the ones it did. See Metrics.

Phone calls (sip)

AssertionPasses when
dtmf_received: { equals: "1234#" }the digits the agent sent, concatenated, equal the value (sip and livekit audio).
call_ended_by: [agent, caller]the call ended for one of the listed reasons: agent, caller, timeout, error, no_answer, busy.
transferred_to: { matches: "^sip:billing@" }the agent transferred the call (a SIP REFER) to a matching target.

These read the call timeline the sip and livekit targets record.

Handoff and escalation

Some conversations must end with a person: the angry customer, the request the agent is not allowed to grant, the user who asks for a human. Others must not. Tell the target how your agent signals a handoff, and assert on it:

targets:
  api:
    type: rest
    url: ${AGENT_URL}
    handoff: { tool: transfer_to_human, to: department, summary: summary }   # or json_path / regex

tests:
  - name: angry customer reaches a person
    target: api
    persona: { name: Ray, style: furious, swears every turn }
    scenario: { goal: "Complain about a late delivery until a human takes over", max_turns: 6 }
    assert:
      - handoff: { within_turns: 3, to: "^support$", summary_contains: ["late"], summary: "The summary says what the customer is angry about" }
      - pack: discloses_ai
  - name: a simple question stays with the agent
    target: api
    scenario: { goal: "Ask how long shipping takes", max_turns: 3 }
    assert:
      - handoff: false

handoff on a target is one of tool (a tool call such as transfer_to_human), json_path (a field of the raw response that is present and not false) or regex (words in the reply); to and summary say where the destination and the summary live (a tool argument name or a JSONPath; with regex, the first group is the destination). When the signal fires the conversation ends with stop reason handoff, a handoff event records the turn, the destination and the summary, and handoff_turn is a metric. A SIP transfer (transferred_to) counts as a handoff to its target.

AssertionPasses when
handoff: truethe agent handed the conversation to a person (a handoff event or a call transfer).
handoff: falseit did not: no over-escalation.
handoff: { within_turns, to, summary_contains, summary }it did, on or before agent turn within_turns, to a destination matching to, with a summary that contains every summary_contains text and that the judge accepts against the summary rubric.

Pair a handoff test with pack: discloses_ai: an agent that escalates well but claims to be a person when asked still fails.

Your backend’s state

A conversation that sounds right can still have done the wrong thing. These two run after the conversation ends and look at your own systems; their templates see {{vars.x}}, {{captured.x}} (a value captured from the agent’s turns, such as an order id) and {{secrets.X}}.

AssertionPasses when
http: { url, method, headers, body, status, json_path, contains, regex, timeout }the request returns the status (any code under 400 when omitted) and the body satisfies every check given: json_path: { path, equals } exists in the JSON body (and equals the value), contains is in the body, regex matches it. method is GET by default; a mapping body is sent as JSON.
command: { run, exit, contains, regex, timeout }the shell command (sh -c) exits with exit (0 when omitted) and its output contains contains and matches regex.
tests:
  - name: refund goes through
    target: api
    capture:
      - { name: order_id, regex: "(ORD-[0-9]+)" }
    scenario: { goal: "Get a refund for the shoes that arrived damaged", max_turns: 6 }
    assert:
      - judge: "The agent confirms the refund and gives a timeline"
      - http:
          url: "{{vars.admin_url}}/orders/{{captured.order_id}}"
          headers: { Authorization: "Bearer {{secrets.ADMIN_TOKEN}}" }
          json_path: { path: "$.status", equals: refunded }
      - command: { run: "./scripts/check-ledger.sh {{captured.order_id}}", contains: credited }

The result quotes the status and the first lines of the body or output as evidence, so a failing check shows what the backend actually held. Both default to a 30-second timeout; on_turn does not apply, since the state is read once, after the conversation.

Models

AssertionPasses when
judge: "rubric"the judge model says the rubric holds. judge: { rubric, model, scale: binary|1-5, min } picks the model and the scale.
evaluator: genkitEval/regex or { name, min }a registered Genkit evaluator scores the run at least min (or reports PASS).
grounded: { context: { tool | json_path | text }, min, model }every factual claim the agent made is supported by the retrieval context (see grounding).

Grounding

An agent that cites documents can still invent. grounded asks the judge model to list every factual claim the agent made and whether the context supports it, and passes when all of them (or min of them, a fraction) are supported. The context is what the agent could legitimately draw on:

  • tool: search_docs: the results of that tool, on the targets where tamesu sees them (model, mcp, realtime, pipecat);
  • json_path: "$.sources[*].text": passages selected from every raw response (rest, websocket, realtime events), for agents that return their sources;
  • text: [...]: passages written in the suite.
tests:
  - name: answers come from the knowledge base
    target: prompt
    scenario: { goal: "Ask how long you have to return shoes and whether there is a restocking fee", max_turns: 4 }
    assert:
      - grounded: { context: { tool: search_docs } }
      - tool_called: search_docs

The result lists each claim with a tick or a cross and the passage behind it, and its score is the supported fraction; tamesu judge calibrate and the replay cache treat the grounding check like any other verdict. Greetings, apologies and questions are not claims.

Custom

custom: name runs an assertion registered from Go with assert.Register; the Go API takes assert.Assertion values directly. See Go API.

Packs

AssertionPasses when
pack: nameevery assertion of the pack passes; the entry expands when the suite loads and each result reads name/<assertion>. Built-in: prompt_injection, pii_leakage, system_prompt_leakage; a suite defines or overrides packs under packs:. See Red-team packs.

One turn only

on_turn: N (1-based) or on_turn: last evaluates the assertion on a single agent turn: its text, its response and its status.

- contains: "anything else"
  on_turn: last
- json_path: { path: $.end_session, equals: true }
  on_turn: last

How a result reads

FAIL   support-agent / refund policy question  2.1s
    x judge "The agent states the 30-day return policy and does not promise an exception": the agent said a 40-day return "should be fine", which promises an exception
    x metric turn_latency_ms_p95 < 4000: turn_latency_ms_p95 = 5120, want < 4000
    last turns:
      user:  So I can still return them?
      agent: Sure, 40 days should be fine, I'll make an exception.

result.json holds the same, plus the judge’s evidence quotes, its trace id and the prompt version.