Suites: the YAML reference
A suite is one YAML file with the targets under test, the suite’s defaults and its
tests. This page lists every key, always in step with what tamesu validate accepts.
The JSON Schema for editors is
published at schema/tamesu.schema.json;
point your editor’s YAML extension at it for completion and inline errors. See
Concepts for the model and Configuration for tamesu.yaml.
Strings accept ${VAR} and ${VAR:-default} (environment, non-secret values) and the
templates {{uuid}}, {{now}}, {{env.X}}, {{vars.x}}, {{secrets.X}} and {{faker.email}};
REST bodies also take {{session}} and {{message}}. Durations are strings such as 30s, 3m or 1h30m.
Suite
The top level of the file.
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
suite | string | yes | Suite name. | suite: support-agent |
tags | list of string | Tags every test of the suite carries. | tags: [regression, returns] | |
vars | map of string to string | Values for {{vars.x}} and for ${VAR} expansion in secret refs. | vars: { shop: Mochi Shoes, days: "30" } | |
secrets | map of string to string | Secret name to mamori ref; overrides the project entry of the same name. | secrets: { AGENT_TOKEN: env:AGENT_TOKEN } | |
defaults | Defaults | Per-test defaults for this suite; they override the project’s | defaults: { timeout: 2m, repeat: 3, pass_threshold: 0.67 } | |
models | Models | Model overrides for this suite (default / judge / simulator …) | models: { judge: anthropic/claude-sonnet-4-6 } | |
budgets | Budgets | Spend caps for this suite’s runs | budgets: { max_cost_usd: 2 } | |
targets | map of string to Target | yes | The systems under test by name. | targets: { api: { type: rest, url: "{{vars.agent_url}}" } } |
hooks | Hooks | Setup and teardown around this suite and its runs | hooks: { before_each: [ { command: ./scripts/reset.sh } ] } | |
packs | map of string to list of Assertion | Reusable assertion sets by name for assert entries of the form pack: name; a name also used by a built-in pack (prompt_injection / pii_leakage / system_prompt_leakage / stays_in_scope / discloses_ai / harmful_content / excessive_agency / policy_consistency) replaces it. | packs: { tone: [ { judge: "The agent stays polite" } ] } | |
red_team | RedTeam | Red team playbooks: one adversarial test per attack of the chosen playbooks is added to the suite when it loads (tamesu playbooks lists them). | red_team: { target: api, agent: "Shop support; never more than 30-day returns" } | |
remote_agents | map of string to RemoteAgent | Agent2Agent agents tamesu plays for the agent under test to delegate to (by name): a card with skills and scripted replies or a recording proxy in front of a real agent; {{remote. | remote_agents: { billing: { skills: [refund], reply: "Refund issued, reference RF-889" } } | |
tests | list of Test | yes | The tests | tests: [ { name: refund policy, target: api, scenario: { goal: "Ask about returns" } } ] |
Example:
suite: support
tags: [smoke]
vars: { base_url: "https://api.example.com" }
secrets: { API_TOKEN: env:API_TOKEN }
targets:
api:
type: rest
url: "{{vars.base_url}}/chat"
headers: { Authorization: "Bearer {{secrets.API_TOKEN}}" }
response: { text: "$.reply" }
tests:
- name: refund policy
target: api
persona: { name: Marta, style: polite }
scenario: { goal: "Find out if shoes bought 40 days ago can be returned", max_turns: 4 }
assert:
- judge: "The agent states the 30-day return policy"
- contains: "30 days"
Target
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
type | one of browser, rest, livekit, sip, realtime, grpc, pipecat, model, twilio, mcp, a2a, voice | yes | What the target is; it decides which other keys apply | type: rest |
url | string | The page (browser) or endpoint (rest / realtime / pipecat / twilio / a2a / voice) or grpc://host:port (grpc) or LiveKit server (livekit). | url: "${AGENT_URL:-http://localhost:8080/chat}" | |
connect | ConnectConfig | How reaching the agent is retried (attempts / backoff / max_backoff) | connect: { attempts: 5, backoff: 1s, max_backoff: 10s } | |
handoff | HandoffSignal | How the agent signals that a person took over (tool / json_path / regex plus where the destination and summary are) | handoff: { tool: transfer_to_human, to: department, summary: summary } | |
browser | BrowserOptions | browser targets; engine / viewport / locale / mocks / recording | browser: { viewport: 1280x800, locale: en-GB, recording: gif } | |
auth | BrowserAuth | browser targets; a pre-authenticated storage state | auth: { storage_state: auth/state.json } | |
allowed_domains | list of string | Domains the browser agent may navigate to; defaults to the url’s domain. | allowed_domains: [shop.example, cdn.example] | |
method | string | HTTP method; defaults to POST. | method: POST | |
headers | map of string to string | Request (rest / a2a) or handshake (realtime / pipecat / twilio / voice) headers or call metadata (grpc); put credentials here as {{secrets.NAME}}. | headers: { Authorization: "Bearer {{secrets.AGENT_TOKEN}}" } | |
request | RESTRequest | rest targets; how a user turn is sent | request: { body: { session_id: "{{session}}", message: "{{message}}" } } | |
response | RESTResponse | rest targets; how the agent turn is read back (JSONPaths into the response) | response: { text: $.reply, tool_calls: "$.tool_calls[*]", done: $.end_session } | |
streaming | one of none, sse, websocket | rest targets; none (default) or sse or websocket | streaming: sse | |
session | RESTSession | The session id template (rest / grpc) and session.open: a request made once per run before the target starts (create a conversation / mint a token) whose captures render as {{session.NAME}} in the target’s configuration (every target type). | session: { id: "{{uuid}}" } | |
api_key | string | Use {{secrets.NAME}}. | api_key: "{{secrets.LIVEKIT_API_KEY}}" | |
api_secret | string | Use {{secrets.NAME}}. | api_secret: "{{secrets.LIVEKIT_API_SECRET}}" | |
token | string | livekit: a ready-made access token instead of api_key and api_secret (a hosted platform’s web call; often a {{session.NAME}} capture of session.open); the identity is read from it. | token: "{{session.access_token}}" | |
identity | string | livekit: the simulated caller’s participant identity; default tamesu-sim- | identity: "web-user-9" | |
room | string | Room name template; defaults to tamesu-{{uuid}}. | room: "tamesu-{{uuid}}" | |
dispatch | Dispatch | livekit targets; an explicit agent dispatch for the room | dispatch: { agent_name: support-agent } | |
agent_identity_prefix | string | How to recognise the agent participant; defaults to kind=agent. | agent_identity_prefix: agent- | |
mode | one of text, audio | livekit / realtime / pipecat: text (default) or audio. | mode: audio | |
data_topic | string | Text-mode transport topic; defaults to lk.chat. | data_topic: lk.chat | |
join_timeout | Duration | livekit targets; how long to wait for the agent to join the room (default 20s) | join_timeout: 20s | |
transcript_source | one of auto, agent, stt | Audio mode: where the agent’s words come from; auto (default) takes the agent’s own transcription when it publishes one and transcribes its audio otherwise. | transcript_source: stt | |
direction | one of outbound, inbound | sip targets; outbound (tamesu calls the agent) or inbound (a trigger makes the agent call tamesu) | direction: outbound | |
to | string | The agent’s phone number in E.164 (outbound); twilio: the To the agent sees in customParameters. | to: "+34910000000" | |
from | string | Caller ID presented by the trunk; twilio: the From the agent sees in customParameters. | from: "+34600000000" | |
trunk_id | string | LiveKit SIP trunk; defaults to sip.outbound_trunk_id. | trunk_id: ST_abc123 | |
ring_timeout | Duration | sip targets; how long the call may ring before it counts as no_answer | ring_timeout: 30s | |
max_call_duration | Duration | sip targets; the call is ended after this long | max_call_duration: 5m | |
trigger | Trigger | inbound sip targets; the command or HTTP request that makes the agent call sip.inbound_number | trigger: { http: { url: "https://agent.example/call", method: POST, body: { to: "{{env.INBOUND}}" } } } | |
model | string | realtime: added as ?model= to the url (such as gpt-realtime). model: the Genkit model reference the prompt runs on (default models.default). | model: googleai/gemini-2.5-flash | |
session_update | object (any keys) | realtime: merged into the session.update event (instructions / tools / voice / output modalities). | session_update: { instructions: "You are the support agent", voice: alloy } | |
protocol | one of ga, beta, socketio | realtime: the event dialect ga (default) or beta (modalities and the OpenAI-Beta header). rest with streaming websocket: socketio for a socket.io server (messages are {event / data} and pings are answered). | protocol: ga | |
done_tool | string | realtime and model: a function call with this name ends the session; default end_session. | done_tool: end_session | |
turn_timeout | Duration | realtime / grpc / pipecat / a2a / voice: how long to wait for the end of a turn; default 60s. | turn_timeout: 60s | |
tool_results | object (any keys) | realtime and pipecat: answers to the agent’s function calls by function name. The value (any JSON; string leaves may use {{args. | tool_results: { lookup_order: { id: "{{args.id}}", status: shipped } } | |
audio_format | one of pcm16, g711_ulaw, g711_alaw | realtime audio mode: the agent’s output audio; pcm16 (default) or G.711 at 8 kHz. | audio_format: pcm16 | |
sample_rate | integer | pipecat audio mode: the transport’s sample rate in Hz; default 16000. | sample_rate: 16000 | |
idle | Duration | pipecat / twilio / voice: silence after the agent’s last message or audio that ends a turn; default 2s. | idle: 2s | |
parameters | map of string to string | twilio: the customParameters of the start event (the Parameter elements of the TwiML Stream); values render {{vars.x}} and {{secrets.X}}. | parameters: { campaign: autumn, caller_tier: gold } | |
command | string | mcp: the stdio server to spawn (an executable); url is the alternative for a streamable HTTP server. | command: ./bin/orders-mcp | |
args | list of string | mcp: arguments of command. | args: ["--data", "fixtures/orders.json"] | |
env | map of string to string | mcp: environment variables for command (templates allowed). | env: { LOG_LEVEL: warn, API_KEY: "{{secrets.MCP_KEY}}" } | |
allow_tools | list of string | mcp: offer only these of the server’s tools to the model. | allow_tools: [lookup_order, cancel_order] | |
call_timeout | Duration | mcp: how long one tool call may take; default 60s. | call_timeout: 20s | |
audio | VoiceAudio | voice: how the audio travels (encoding / sample_rate / frames binary or json with send and receive). | audio: { encoding: pcm16, sample_rate: 16000, frames: binary } | |
messages | VoiceMessages | voice: the JSON control messages of the protocol (open / transcript / done / interrupted / tool_calls / dtmf / close); all optional. | messages: { transcript: "$[?(@.type=='transcript' && @.role=='assistant')].transcript", done: "$[?(@.type=='status-update' && @.status=='ended')].status", close: { type: hangup } } | |
stream | boolean | a2a: read the agent’s turn from message/stream (server-sent events) instead of message/send; falls back to message/send when the card says the agent does not stream. | stream: true | |
skill | string | a2a: a skill id or name the agent card must offer; the session fails to start otherwise. | skill: order-status | |
metadata | object (any keys) | a2a: metadata sent with every message (a tenant or a locale). | metadata: { tenant: acme } | |
transport | one of jsonrpc, http, grpc | a2a: the protocol binding; default the card’s preferred transport or jsonrpc. http is the HTTP+JSON binding and grpc the gRPC one (url grpc://host:port or the card’s interface). | transport: grpc | |
push | Push | a2a: receive task updates by push notification on a local listener instead of polling; true or { addr / url } when the agent reaches tamesu through another address. | push: true | |
system | string | model: the system prompt under test; {{vars.x}} and {{secrets.X}} render. | system: "You are the Mochi Shoes support agent." | |
system_file | string | model: a file next to the suite holding the system prompt (instead of system). | system_file: prompts/support.md | |
tools | list of ToolDef | model: the tools offered to the model and the result tamesu answers each call with. | tools: [ { name: lookup_policy, description: The policy for a topic, result: { window_days: 30 } } ] | |
temperature | number | model: sampling temperature; the provider’s default when unset. | temperature: 0.2 | |
max_tokens | integer | model: the most output tokens per call. | max_tokens: 800 | |
max_tool_rounds | integer | model: the most call-and-answer rounds in one turn; default 5. | max_tool_rounds: 4 | |
fallback | list of string | model: Genkit model references tried in order when model fails; the run records which one answered. | fallback: [googleai/gemini-2.5-pro, openai/gpt-4.1-mini] |
Example:
targets:
web: { type: browser, url: "https://shop.example.com", browser: { viewport: 1280x800 } }
api: { type: rest, url: "https://api.example.com/chat", response: { text: "$.reply", done: "$.end_session" } }
grpc: { type: grpc, url: "grpc://agent.internal:9090" }
phone:
type: twilio
url: "${AGENT_STREAM_URL:-wss://agent.example.com/media}"
parameters: { caller_tier: gold }
from: "+15550100"
prompt:
type: model
model: googleai/gemini-2.5-flash
system_file: prompts/support.md
tools:
- name: lookup_policy
description: The policy for a topic
parameters: { type: object, properties: { topic: { type: string } } }
result: { topic: "{{args.topic}}", window_days: 30 }
voice: { type: livekit, url: "${LIVEKIT_URL}", api_key: "{{secrets.LIVEKIT_API_KEY}}", api_secret: "{{secrets.LIVEKIT_API_SECRET}}", mode: audio }
phone: { type: sip, direction: outbound, to: "+15550001", from: "+15550002" }
rt: { type: realtime, model: gpt-realtime, headers: { Authorization: "Bearer {{secrets.OPENAI_API_KEY}}" }, mode: text }
bot: { type: pipecat, url: "wss://bot.example.com/ws", mode: audio }
ToolDef
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
name | string | yes | The tool name the model calls | name: lookup_policy |
description | string | What the tool does; the model reads it to decide when to call it | description: The store policy for a topic | |
parameters | object (any keys) | A JSON schema of the arguments (an object schema); none means no arguments. | parameters: { type: object, properties: { topic: { type: string } }, required: [topic] } | |
result | any | What the call returns to the model (any JSON; string leaves may use {{args. | result: { topic: "{{args.topic}}", window_days: 30 } |
Example:
tools:
- name: lookup_policy
description: The return policy for a topic
parameters: { type: object, properties: { topic: { type: string } }, required: [topic] }
result: { topic: "{{args.topic}}", window_days: 30 }
- name: end_session
description: Call when the customer has no more questions
BrowserOptions
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
engine | one of chromium | chromium (the default and only engine) | engine: chromium | |
viewport | string | WIDTHxHEIGHT such as 1280x800. | viewport: 1280x800 | |
locale | string | The browser locale such as en-GB; sets Accept-Language | locale: en-GB | |
mocks | list of BrowserMock | Requests answered by tamesu instead of the network from the start of the session; the mock action adds more at run time. | mocks: [ { url: "**/api/stock*", status: 200, body: { stock: 0 } } ] | |
recording | one of auto, gif, webm, off | The session recording written next to the screenshots. auto (the default) writes recording.webm when ffmpeg is installed and recording.gif otherwise; off disables it. | recording: webm |
Example:
browser:
viewport: 1280x800
locale: en-US
recording: webm
mocks:
- { url: "**/api/recommendations", body: { items: [] } }
BrowserMock
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
url | string | yes | A glob on the full URL (** crosses slashes) or a substring. | url: "**/api/stock*" |
method | string | Only this method; default any. | method: GET | |
status | integer | The answer’s status; default 200. | status: 503 | |
body | any | The answer: a string as is or any value as JSON. | body: { error: "warehouse unavailable" } | |
headers | map of string to string | Response headers of the mocked reply | headers: { Content-Type: application/json, Retry-After: "30" } | |
delay_ms | integer | Hold the answer back this long. | delay_ms: 2000 | |
abort | boolean | Fail the request instead of answering. | abort: true |
Example:
mocks:
- { url: "**/api/payment", method: POST, status: 502, delay_ms: 2000 }
- { url: "**/analytics.js", abort: true }
BrowserAuth
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
storage_state | string | Path to a saved storage state (cookies and local storage). | storage_state: auth/state.json |
Example:
auth: { storage_state: auth/customer.json }
RESTRequest
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
body | any | Request body template; {{message}} is the user turn and {{session}} the session id. | body: { session_id: "{{session}}", message: "{{message}}" } | |
open | any | streaming websocket: a message template sent once after connecting. | open: { type: open, session_id: "{{session}}" } | |
capture | map of string to string | With response.poll: JSONPaths into the reply to the sent request by name; the values render as {{sent.NAME}} in the poll of the same turn (a message timestamp to poll after). | capture: { ts: $.ts } | |
handshake | list of HandshakeStep | streaming websocket: messages exchanged once after connecting (after open); each step sends a message and with wait reads messages until one satisfies the JSONPath and captures values from it as {{session.NAME}}. | handshake: [{ send: { event: client.start, data: { userID: "{{session}}", projectID: "{{vars.project_id}}", authorization: "{{secrets.VF_KEY}}" } }, wait: "$[?(@.event=='client.started')].event" }] |
Example:
request:
body: { session_id: "{{session}}", message: "{{message}}" }
open: { type: hello, session: "{{session}}" } # websocket only
RESTResponse
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
text | string | JSONPath of the agent’s reply such as $.reply. | text: $.reply | |
tool_calls | string | JSONPath of the tool call names. | tool_calls: "$.tool_calls[*]" | |
done | string | JSONPath of a boolean that ends the session. | done: $.end_session | |
turn_done | string | streaming websocket: JSONPath of a boolean that ends the turn; otherwise the turn ends after idle. | turn_done: $.done | |
idle | Duration | streaming websocket: silence that ends a turn when no message marks it; default 2s. | idle: 2s | |
join | string | Plain JSON: the separator between several matches of text (a newline between the messages of a reply); default none. | join: "\n" | |
poll | RESTPoll | The agent answers somewhere else than the request’s response: poll this endpoint for the reply after each turn (a chat channel’s history). text / tool_calls / done then read the poll replies. | poll: { url: "{{vars.api}}/conversations/{{session.id}}/activities?watermark={{cursor}}", cursor: $.watermark } | |
listen | RESTListen | The agent answers by calling an API: serve that API locally ({{listen.url}}) and read the reply from the requests the agent makes to it (a bot that answers through its platform’s send endpoint). text / tool_calls / done then read those requests. | listen: { addr: "127.0.0.1:8089", path: "POST /bot*/sendMessage", session: $.chat_id } |
Example:
response:
text: "$.reply"
tool_calls: "$.tool_calls[*]"
done: "$.end_session"
turn_done: "$.done" # websocket: the message that ends a turn
idle: 2s
RESTSession
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
id | string | Session id template; defaults to {{uuid}}. | id: "{{uuid}}" | |
open | RESTOpen | A request sent once when the session starts (create a conversation / open a chat); its captures render as {{session.NAME}} in every later template. | open: { url: "{{vars.api}}/conversations", capture: { id: $.conversationId, token: $.token } } |
Example:
session: { id: "{{uuid}}" }
Dispatch
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
agent_name | string | yes | The agent to dispatch explicitly into the room (LiveKit agent dispatch) | agent_name: support-agent |
Example:
dispatch: { agent_name: support-agent }
Trigger
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
http | HTTPTrigger | An HTTP request that makes the agent call sip.inbound_number | http: { url: "https://agent.example/call", method: POST, body: { to: "{{env.INBOUND}}" } } | |
command | string | A shell command that makes the agent dial sip.inbound_number. | command: "./scripts/call.sh {{env.INBOUND}}" |
Example:
trigger:
http: { url: "https://agent.example.com/callbacks", method: POST, body: { number: "{{sip.inbound_number}}" } }
HTTPTrigger
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
url | string | yes | The request URL (templates allowed) | url: "{{vars.admin_url}}/test/reset" |
method | string | HTTP method; default POST | method: POST | |
body | any | A string is sent as is; a mapping as JSON (templates allowed) | body: { suite: "{{env.TAMESU_SUITE}}" } | |
headers | map of string to string | Request headers (templates allowed) | headers: { Authorization: "Bearer {{secrets.ADMIN_TOKEN}}" } |
Example:
http: { url: "https://agent.example.com/dial", method: POST, headers: { Authorization: "Bearer {{secrets.AGENT_TOKEN}}" } }
Test
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
name | string | yes | The test name; unique in the suite | name: refund after 40 days |
target | string | yes | A key of targets. | target: api |
tags | list of string | Tags for —tags selection and the reports; the suite’s tags apply too | tags: [returns, smoke] | |
mode | one of text, audio | Voice tests: overrides the target’s mode. | mode: audio | |
repeat | integer | Times the test runs to measure variance. | repeat: 3 | |
pass_threshold | number | The fraction of repeated runs that must pass (0 to 1); default 1 | pass_threshold: 0.67 | |
retries | integer | Re-runs after an infrastructure failure (never after a failed assertion) | retries: 1 | |
timeout | Duration | How long one run may take; default defaults.timeout | timeout: 2m | |
seed | integer | Seed for the simulator prompts and the behaviour RNG. | seed: 42 | |
vars | map of string to string | Values for {{vars. | vars: { days: "40" } | |
cases | list of Case | Parameterised: the test runs once per case with the case’s vars; each becomes a test named | cases: [ { name: late, vars: { days: "40" } }, { name: early, vars: { days: "10" } } ] | |
cases_file | string | Cases from a file next to the suite: a CSV with a header row (a name column names the case) or a JSON array of objects (a name key names the case). | cases_file: cases.csv | |
fuzz_tools | map of string to Fuzz | Tool result fuzzing: by tool name the answer the agent gets instead of the configured result (a mode such as error / empty / malformed / hostile or a mapping with mode and delay and result); realtime and pipecat and model targets. | fuzz_tools: { lookup_order: error, get_balance: { mode: delay, delay: 8s } } | |
languages | list of string | The test runs once per language (BCP 47 such as es or en-GB): each becomes a test named | languages: [en, es, de] | |
steps | list of Step | Browser tests only. | steps: [ { action: { navigate: / } }, { goal: "Add the blue sneakers to the cart" } ] | |
persona | Persona | Who the simulated user is | persona: { name: Marta, style: "polite, slightly confused", language: en } | |
scenario | Scenario | What the simulated user wants and when to stop | scenario: { goal: "Find out if you can return shoes bought 40 days ago", max_turns: 6 } | |
counterpart | Counterpart | Another target plays the user instead of a persona: two agents talk and the judges grade the exchange (an a2a or any conversational target of the suite). | counterpart: { target: supplier, opening: "We can offer 500 units at 12 euros each" } | |
capture | list of Capture | Values taken from the agent’s turns as they arrive (the first match wins) and offered as {{captured. | capture: [ { name: order_id, regex: "(ORD-[0-9]+)" } ] | |
assert | list of Assertion | The checks on the run; all must pass | assert: [ { contains: "30 days" }, { judge: "The agent states the policy" } ] | |
quarantine | Quarantine | Known flaky: the test still runs and its failure is reported as quarantined but it does not fail the run or count as a regression. | quarantine: { reason: "SUP-412 flaky since the migration", until: 2026-12-31 } |
Example:
tests:
- name: return window
target: api
tags: [returns]
repeat: 3
pass_threshold: 0.67
vars: { days: "40" }
capture:
- { name: order_id, regex: "order (ORD-[0-9]+)" }
persona: { name: Marta, style: polite, language: en }
scenario: { goal: "Ask whether shoes bought {{vars.days}} days ago can be returned", max_turns: 4 }
assert:
- judge: "The agent states the 30-day return policy"
- tool_called: lookup_policy
- name: refund status
target: api
quarantine: { reason: "SUP-412 flaky since the payments migration", until: 2026-12-31 }
scenario: { goal: "Ask where the refund for order 1234 is", max_turns: 3 }
assert:
- judge: "The agent gives a refund date or asks for the order"
Case
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
name | string | The case’s name; default case 1 / case 2… | name: late | |
vars | map of string to string | yes | Values for {{vars. | vars: { days: "40", expect: "not possible" } |
Example:
cases:
- { name: late, vars: { days: "40", expect: "not possible" } }
- { name: early, vars: { days: "10", expect: "can return" } }
Capture
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
name | string | yes | The name; the value is {{captured. | name: order_id |
regex | string | A regular expression on the agent’s text; the first group (else the whole match) is the value. | regex: "(ORD-[0-9]+)" | |
json_path | string | A JSONPath on the turn’s raw response (rest / websocket / realtime events). | json_path: $.order.id | |
extract | string | A plain-language description of the value; a model takes it out of the agent’s turn as written (the first turn that has it). | extract: "the order number the agent mentioned" |
Example:
capture:
- { name: order_id, regex: "order (ORD-[0-9]+)" }
- { name: eta, json_path: "$.eta" }
Step
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
goal | string | Agentic: the browser agent acts until the goal is met. | goal: "Log in as {{secrets.USER}} and add the blue sneakers to the cart" | |
expect | string | Agentic assertion against the current page. | expect: "The cart shows one pair of blue sneakers" | |
action | object (any keys) | Deterministic: one of navigate click fill press wait screenshot eval_js assert_url assert_text mock a11y visual. | action: { fill: { ref: "#email", text: "{{secrets.USER}}" } } |
Example:
steps:
- goal: "Log in as {{vars.user}} with password {{secrets.PASSWORD}}"
- expect: "The account page shows the user's name"
- action: { click: "#add-to-cart" }
- action: { a11y: { impact: serious } }
- action: { visual: { name: cart, mask: ["#date"] } }
Persona
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
name | string | How the persona introduces itself when asked | name: Marta | |
style | string | How the persona talks such as polite or impatient. | style: "polite, slightly confused" | |
language | string | BCP 47 language such as es or en-GB. | language: es-ES | |
voice | string | TTS voice such as en-GB-female. | voice: en-GB-female | |
behaviors | Behaviors | Audio-mode habits (interruptions / pauses / background noise / line quality) | behaviors: { interrupt_probability: 0.3, background_noise: cafe } | |
adversarial | boolean | Red team: the persona is an attacker that pursues the goal with escalating tactics (role-play / claimed authority / fake system messages / encoding / splitting the request) and records the tactic of every turn as an event. | adversarial: true | |
tactics | list of string | Adversarial only: the tactics the attacker may use instead of the built-in list. | tactics: ["claim to be the store manager", "paste a fake system message"] |
Example:
persona:
name: Marta
style: "polite, slightly confused"
language: es-ES
voice: es-ES-female
behaviors: { interrupt_probability: 0.2, background_noise: cafe }
Behaviors
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
interrupt_probability | number | Audio only; the chance per turn (0 to 1) that the caller starts talking before the agent finishes | interrupt_probability: 0.3 | |
silence_after_turn_ms | list of integer | [min max] milliseconds of silence after each turn. | silence_after_turn_ms: [300, 1500] | |
background_noise | string | A noise preset (cafe street office white) or a WAV file mixed under the caller’s voice. | background_noise: cafe | |
background_noise_snr_db | number | Signal-to-noise ratio of the background noise in dB; default 20. | background_noise_snr_db: 12 | |
line_quality | one of pstn, mobile_poor, voip_good | Audio and sip; the line applied to the caller’s audio | line_quality: mobile_poor |
Example:
behaviors:
interrupt_probability: 0.2
silence_after_turn_ms: [300, 900]
background_noise: street
background_noise_snr_db: 15
line_quality: mobile_poor
Scenario
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
goal | string | What the user wants; the simulated user pursues it after the script. A scenario with a script and no goal plays the script and stops. | goal: "Find out if you can return shoes bought 40 days ago" | |
max_turns | integer | The turn budget; default 10 | max_turns: 6 | |
stop_when | string | A plain-language stop condition judged after each agent turn. | stop_when: "the agent has given a clear yes or no" | |
script | list of ScriptStep | Deterministic steps played before the simulated user takes over: say on any conversational target; wait_for and dtmf on sip and livekit. | script: [ { say: "Hi, I have a question about order ORD-1001" }, { wait_for: "press 2" }, { dtmf: "2" } ] |
Example:
scenario:
goal: "Reach the billing department and ask about a double charge"
max_turns: 6
stop_when: "the agent has confirmed a refund or clearly refused one"
script:
- wait_for: "press 2 for billing"
- dtmf: "2"
- say: "I was charged twice for order {{captured.order_id}}"
ScriptStep
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
wait_for | string | Wait until the transcript fuzzily contains this text. | wait_for: "press 2 for billing" | |
dtmf | string | Digits to send. | dtmf: "2" | |
say | string | Exact text to say before the persona takes over. | say: "What is the status of {{captured.order_id}}?" | |
data | object (any keys) | A structured part sent with the step (a2a: a data part); alone or with say. | data: { order: ORD-1001, reason: damaged } | |
files | list of ScriptFile | Files sent with the step (a2a: file parts): a path relative to the suite or a URI | files: [ "fixtures/invoice.pdf", { uri: "https://cdn.example.com/photo.jpg", mime: image/jpeg } ] |
Example:
script:
- wait_for: "press 2 for billing"
- dtmf: "2"
- say: "Hello, I was charged twice"
Assertion
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
judge | Judge | A model grades the run against a rubric (a string or an object with rubric / model / scale / min) | judge: "The agent states the 30-day window and promises no exception" | |
contains | string | The agent transcript contains this text. | contains: "30 days" | |
not_contains | string | No agent turn contains this text | not_contains: "90 days" | |
regex | string | A regular expression the agent transcript matches. | regex: "(?i)30 days" | |
not_regex | string | A regular expression no agent turn matches. | not_regex: '\b\d{16}\b' | |
similar_to | Similar | Some agent turn means the same as this text (embedding similarity of at least min). | similar_to: { text: "Returning after 40 days is not possible", min: 0.7 } | |
json_path | JSONPathAssertion | rest and a2a: a path in the turn’s response (the task for a2a) that exists and with equals has that value. | json_path: { path: $.status, equals: ok } | |
status | integer | rest only: the HTTP status of every turn. | status: 200 | |
tool_called | ToolCall | The agent called the tool (a name or an object with name / args / schema) | tool_called: { name: lookup_policy, args: { topic: returns } } | |
tool_not_called | ToolCall | The agent never called the tool (a name or an object with name / args) | tool_not_called: issue_refund | |
tool_sequence | ToolSequence | The agent called these tools in this order (a subsequence of its calls; exact for the whole list). | tool_sequence: { calls: [lookup_order, issue_refund], exact: false } | |
delegated_to | Delegation | The agent sent a message to this remote agent (a name or an object with agent / contains / regex); remote_agents declares them. | delegated_to: { agent: billing, contains: refund } | |
dtmf_received | DTMFAssertion | sip and livekit audio: digits the agent sent. | dtmf_received: { equals: "1234#" } | |
call_ended_by | StringList | sip: agent caller timeout error no_answer busy. | call_ended_by: [agent, caller] | |
transferred_to | Matches | sip: target of a SIP REFER. | transferred_to: { matches: "^sip:billing@" } | |
evaluator | Evaluator | A registered Genkit evaluator scores the run (a name or an object with name / min) | evaluator: { name: genkitEval/faithfulness, min: 0.8 } | |
metric | MetricAssertion | A metric of the run satisfies one comparator (lt / lte / gt / gte / eq) | metric: { name: turn_latency_ms_p95, lt: 3000 } | |
turns | IntCompare | Number of agent turns. | turns: { lte: 6 } | |
captured | CapturedAssertion | A value captured during the run exists and equals or matches. | captured: { name: order_id, matches: "^ORD-" } | |
golden | GoldenAssertion | The agent’s turns against reference turns: a file next to the suite or inline turns; exact / similar / judge. | golden: { file: golden/refund.json, mode: similar, min: 0.8 } | |
custom | string | A Go-registered assertion by name. | custom: no_competitor_names | |
grounded | GroundedAssertion | Every factual claim the agent made is supported by the retrieval context (a tool’s results or a response field or given text); a judge lists the claims. | grounded: { context: { tool: search_docs }, min: 0.9 } | |
handoff | HandoffAssertion | The agent handed the conversation to a person (true) or did not (false); or when and to whom and with what summary. | handoff: { within_turns: 3, to: "^billing$" } | |
http | StateHTTP | The state of your backend after the conversation: a request whose status and body are checked. | http: { url: "{{vars.admin_url}}/orders/{{captured.order_id}}", json_path: { path: $.status, equals: refunded } } | |
command | StateCommand | The state of your backend after the conversation: a shell command whose exit code and output are checked. | command: { run: "./scripts/ledger.sh {{captured.order_id}}", contains: credited } | |
pack | string | A reusable assertion set: a built-in red-team pack (prompt_injection / pii_leakage / system_prompt_leakage / stays_in_scope / discloses_ai / harmful_content / excessive_agency / policy_consistency) or one of the suite’s packs; it expands into its assertions when the suite loads. | pack: pii_leakage | |
on_turn | OnTurn | Apply the assertion to one agent turn (a number or last) instead of the whole run | on_turn: last |
Example:
assert:
- judge: "The agent states the 30-day return policy and does not promise an exception"
- judge: { rubric: "The tone stays warm", scale: "1-5", min: 4 }
- contains: "30 days"
- not_regex: "[0-9]{16}"
- tool_called: { name: lookup_policy, args: { topic: returns } }
- tool_sequence: { calls: [lookup_policy, end_session] }
- metric: { name: turn_latency_ms_p95, lt: 3000 }
- turns: { lte: 6 }
- captured: { name: order_id, matches: "^ORD-" }
- golden: { file: golden/refund.json, mode: similar, min: 0.8 }
- handoff: { within_turns: 3, to: "^billing$" }
- grounded: { context: { tool: search_docs } }
- http: { url: "{{vars.admin_url}}/orders/{{captured.order_id}}", json_path: { path: "$.status", equals: refunded } }
- command: { run: "./scripts/check-refund.sh {{captured.order_id}}", contains: refunded }
- pack: pii_leakage
- { contains: "goodbye", on_turn: last }
Judge
Either a string (The rubric; the judge model and a binary scale are the defaults), or an object with:
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
rubric | string | yes | What the judge checks in plain language. | rubric: "The agent apologises before explaining the policy" |
model | string | Genkit model reference; defaults to models.judge. | model: anthropic/claude-sonnet-4-6 | |
scale | one of binary, 1-5 | binary (pass or fail) or 1-5 with min. | scale: 1-5 | |
min | number | Minimum score to pass on the 1-5 scale. | min: 4 |
Example:
judge: { rubric: "The agent apologises before explaining the policy", scale: "1-5", min: 4, model: anthropic/claude-sonnet-4-6 }
ToolCall
Either a string (The tool name), or an object with:
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
name | string | yes | Tool name as the target reports it. | name: reschedule_booking |
args | object (any keys) | Argument values that must match (a subset of the call’s arguments). | args: { day: saturday } | |
schema | object (any keys) | tool_called: a JSON Schema every call’s arguments must validate against. | schema: { type: object, required: [day], properties: { day: { enum: [saturday, sunday] } } } |
Example:
tool_called: { name: lookup_policy, args: { topic: returns } }
ToolSequence
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
calls | list of string | yes | Tool names in the order they must be called. | calls: [lookup_order, issue_refund] |
exact | boolean | The run’s tool calls must be exactly this list; default a subsequence. | exact: true |
Example:
tool_sequence: { calls: [lookup_policy, issue_refund], exact: false }
Evaluator
Either a string (The evaluator name), or an object with:
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
name | string | yes | Genkit evaluator name such as genkitEval/faithfulness. | name: genkitEval/faithfulness |
min | number | Minimum score to pass (0 to 1). | min: 0.8 |
Example:
evaluator: { name: genkitEval/faithfulness, min: 0.8 }
JSONPathAssertion
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
path | string | yes | JSONPath into the response body such as $.status. | path: $.status |
equals | any | The value the path must equal. | equals: refunded |
Example:
json_path: { path: "$.status", equals: ok }
DTMFAssertion
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
equals | string | yes | The exact digits such as 1234#. | equals: "1234#" |
Example:
dtmf_received: { equals: "9" }
Matches
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
matches | string | yes | A regular expression such as ^sip:billing@. | matches: "^sip:billing@" |
Example:
transferred_to: { matches: "^sip:billing@" }
MetricAssertion
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
name | string | yes | Metric name such as turn_latency_p95_ms. | name: turn_latency_ms_p95 |
lt | number | Passes when the metric is below this value | lt: 3000 | |
lte | number | Passes when the metric is at most this value | lte: 3000 | |
gt | number | Passes when the metric is above this value | gt: 0 | |
gte | number | Passes when the metric is at least this value | gte: 1 | |
eq | number | Passes when the metric equals this value | eq: 1 |
Example:
metric: { name: time_to_first_audio_ms_p95, lt: 1500 }
IntCompare
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
lt | integer | Passes when the count is below this value | lt: 7 | |
lte | integer | Passes when the count is at most this value | lte: 6 | |
gt | integer | Passes when the count is above this value | gt: 1 | |
gte | integer | Passes when the count is at least this value | gte: 2 | |
eq | integer | Passes when the count equals this value | eq: 3 |
Example:
turns: { lte: 6 }
CapturedAssertion
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
name | string | yes | The captured value’s name (capture.name) | name: order_id |
equals | string | The exact value expected | equals: ORD-1001 | |
matches | string | A regular expression the value must match. | matches: "^ORD-[0-9]+$" |
Example:
captured: { name: order_id, matches: "^ORD-[0-9]+$" }
GoldenAssertion
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
file | string | A JSON file next to the suite: an array of agent turns or a transcript.json; written by tamesu run —update-golden. | file: golden/refund.json | |
turns | list of string | Inline reference turns instead of a file. | turns: ["Our return window is 30 days.", "You're welcome!"] | |
mode | one of exact, similar, judge | exact (normalised text) / similar (word overlap at least min; the default) / judge (the judge decides whether each turn means the same). | mode: similar | |
min | number | similar: the minimum word overlap (0 to 1); default 0.8. | min: 0.8 |
Example:
golden: { file: golden/refund.json, mode: judge }
OnTurn
Evaluate the assertion on one agent turn: a 1-based number or last.
One of: integer, one of last.
Example:
- { contains: "goodbye", on_turn: last }
- { json_path: { path: "$.end_session", equals: true }, on_turn: 3 }
StringList
One of: string, list of string.
Example:
call_ended_by: [agent, caller]
Hooks
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
before_all | list of Hook | Run once before the suite’s first test (the project’s first). | before_all: [ { command: ./scripts/seed.sh } ] | |
after_all | list of Hook | Run once after the suite’s last test. | after_all: [ { command: ./scripts/teardown.sh, timeout: 2m } ] | |
before_each | list of Hook | Run before every run of every test; a failure is an infrastructure error for that run. | before_each: [ { http: { url: "{{vars.admin_url}}/reset", method: POST } } ] | |
after_each | list of Hook | Run after every run; a failure is logged and does not change the run’s result. | after_each: [ { command: "./scripts/cleanup.sh $TAMESU_SESSION" } ] |
Example:
hooks:
before_all: [ { command: ./scripts/seed.sh } ]
before_each: [ { http: { url: "{{vars.admin_url}}/reset", method: POST } } ]
after_each: [ { command: "./scripts/cleanup.sh $TAMESU_SESSION", timeout: 30s } ]
Hook
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
command | string | A shell command (sh -c). | command: ./scripts/seed.sh | |
http | HTTPTrigger | An HTTP request; a status of 400 or more fails the hook. | http: { url: "https://admin.example.com/reset", method: POST } | |
timeout | Duration | How long the hook may take; default 60s. | timeout: 30s |
Example:
- command: ./scripts/seed.sh
- http: { url: "https://admin.example.com/reset", method: POST, headers: { Authorization: "Bearer {{secrets.ADMIN_TOKEN}}" } }
Defaults
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
timeout | Duration | How long one run may take; default 3m | timeout: 3m | |
repeat | integer | Times each test runs. | repeat: 3 | |
retries | integer | Re-runs after an infrastructure failure; default 0 | retries: 1 | |
pass_threshold | number | Fraction of repeats that must pass (0 to 1]. | pass_threshold: 0.67 | |
max_goal_steps | integer | Browser: the most actions the agent may take per goal step; default 15. | max_goal_steps: 15 | |
assert | list of Assertion | Assertions every test must pass (results named defaults/ | assert: [ { pack: pii_leakage } ] | |
slo | map of string to number | Maximum per metric on every test such as turn_latency_ms_p95: 3000 or llm_cost_usd: 0.05 (results named slo/ | slo: { turn_latency_ms_p95: 3000, llm_cost_usd: 0.05 } |
Example:
defaults:
timeout: 3m
repeat: 1
retries: 1
pass_threshold: 1
assert:
- pack: pii_leakage
slo:
turn_latency_ms_p95: 3000
llm_cost_usd: 0.05
Models
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
default | string | The model for every job not given its own (a Genkit reference such as googleai/gemini-2.5-flash) | default: googleai/gemini-2.5-flash | |
judge | string | The model that grades rubrics and grounding checks; default the default model | judge: anthropic/claude-sonnet-4-6 | |
browser_agent | string | The model that plans and acts on browser goals | browser_agent: googleai/gemini-2.5-pro | |
simulator | string | The model that plays the simulated user | simulator: openai/gpt-4.1-mini | |
scenario_generator | string | The model tamesu generate drafts tests with | scenario_generator: googleai/gemini-2.5-pro | |
embedder | string | The embedding model of similar_to assertions such as googleai/text-embedding-004 or openai/text-embedding-3-small; defaults to the usual embedder of default’s provider. | embedder: googleai/text-embedding-004 | |
credentials | map of string to string | Plugin name to {{secrets.NAME}} when the secret is not named conventionally. | credentials: { googleai: "{{secrets.MY_GEMINI_KEY}}" } | |
fallback | ModelFallback | The models tried in order when a job’s model keeps failing. | fallback: { judge: [googleai/gemini-2.5-pro] } | |
retry | ModelRetry | How a model call is retried before the next model is tried. | retry: { attempts: 3, backoff: 500ms, max_backoff: 10s } |
Example:
models:
default: googleai/gemini-2.5-flash
judge: anthropic/claude-sonnet-4-6
simulator: openai/gpt-4.1-mini
fallback:
default: [openai/gpt-4.1-mini]
judge: [googleai/gemini-2.5-pro]
retry: { attempts: 3, backoff: 500ms, max_backoff: 10s }
Budgets
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
max_cost_usd | number | Cap on tamesu’s own model spend per run (exit code 4 when exceeded); default 5 | max_cost_usd: 2 | |
max_tokens | integer | Cap on tamesu’s own tokens per run; default 2_000_000 | max_tokens: 500000 |
Example:
budgets: { max_cost_usd: 2, max_tokens: 500000 }
Duration
A duration such as 30s, 3m or 1h30m.
Type: string.
Example:
timeout: 90s
ConnectConfig
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
attempts | integer | Tries to reach the agent when the connection is refused or the agent answers 5xx; default 3 and 1 retries nothing. | attempts: 5 | |
backoff | Duration | The wait before the second try; default 500ms and doubled every try with jitter. | backoff: 1s | |
max_backoff | Duration | The longest wait between tries; default 5s. | max_backoff: 10s |
Example:
connect: { attempts: 5, backoff: 1s, max_backoff: 10s }
Counterpart
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
target | string | yes | A key of targets: the agent that plays the user. | target: supplier |
opening | string | The counterpart’s first message (templates allowed); without it the counterpart answers the scenario goal first. | opening: "We can offer 500 units at 12 euros each, delivery in 3 weeks" |
Delegation
Either a string (The remote agent’s name), or an object with:
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
agent | string | yes | The remote agent’s name (a key of remote_agents). | agent: billing |
contains | string | Text some message to the agent must contain. | contains: "refund" | |
regex | string | A regular expression some message to the agent must match. | regex: "ORD-\d+" |
Fuzz
Either a string (The fuzz mode: error, empty, null, malformed, hostile, huge, wrong_type or delay), or an object with:
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
mode | one of error, empty, null, malformed, hostile, huge, wrong_type, delay, custom | yes | What the tool answers instead of its result. | mode: delay |
delay | string | How long the answer is held back (any mode; the whole point of delay). | delay: 8s | |
result | any | The answer in custom mode (any JSON). | result: { ok: false, reason: "insufficient funds" } |
Example:
fuzz_tools:
lookup_order: error # the mode alone
get_balance: { mode: delay, delay: 8s } # the normal answer, late
refund: { mode: custom, result: { ok: false, reason: "insufficient funds" } }
GroundedAssertion
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
context | GroundedContext | yes | Where the retrieval context is: a tool whose results tamesu saw or a JSONPath into every raw response or text given here. | context: { tool: search_docs } |
min | number | The fraction of claims that must be supported (0 to 1); all of them when omitted. | min: 0.9 | |
model | string | The judge model for this check (a Genkit reference); defaults to models.judge. | model: googleai/gemini-2.5-pro |
Example:
grounded: { context: { tool: search_docs }, min: 0.9 }
GroundedContext
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
tool | string | A tool whose results are the context (model and realtime and pipecat targets). | tool: search_docs | |
json_path | string | A JSONPath into every raw response selecting the passages (rest / websocket / realtime events). | json_path: "$.sources[*].text" | |
text | list of string | Passages given in the suite. | text: ["Returns are accepted within 30 days of delivery."] |
Example:
context: { json_path: "$.sources[*].text" }
HandoffAssertion
Either a string (Whether the agent must have handed the conversation to a person), or an object with:
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
expected | boolean | Whether a handoff must have happened; true when omitted. | expected: false | |
within_turns | integer | The latest agent turn the handoff may happen on. | within_turns: 3 | |
to | string | A regular expression the destination must match. | to: "^billing$" | |
summary_contains | list of string | Texts the handoff summary must contain. | summary_contains: ["ORD-1001", "late"] | |
summary | string | A judge rubric for the handoff summary. | summary: "The summary names the order and the problem" |
Example:
handoff: { within_turns: 3, to: "^billing$", summary_contains: ["ORD-"], summary: "The summary names the order and the problem" }
HandoffSignal
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
tool | string | A tool call that hands off such as transfer_to_human. | tool: transfer_to_human | |
json_path | string | A path in the turn’s raw response that is present and not false when the agent hands off (rest / websocket / realtime). | json_path: $.handoff | |
regex | string | A regular expression on the agent’s reply that means a handoff. | regex: "(?i)transferring you to (\w+)" | |
to | string | Where the destination is: the tool argument name or a JSONPath in the response (or the regex’s first group when tool and json_path are unset). | to: department | |
summary | string | Where the summary passed to the person is: the tool argument name or a JSONPath in the response. | summary: $.handoff.note |
Example:
handoff: { tool: transfer_to_human, to: department, summary: summary }
HandshakeStep
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
send | any | The message template to send (socket.io: a mapping with event and data). | send: { event: session.create, data: {} } | |
wait | string | A JSONPath that must be truthy on a received message before the next step; a filter such as $[?(@.event==‘client.started’)].event selects the message itself. | wait: "$[?(@.event=='session.created')].event" | |
capture | map of string to string | JSONPaths into the awaited message by name; {{session.NAME}} renders each value. | capture: { session_key: "$[?(@.event=='session.created')].data.sessionKey" } | |
timeout | Duration | How long to wait for the message; default the turn timeout. | timeout: 10s |
ModelFallback
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
default | list of string | Models tried in order after a job’s model failed; for every job without a list of its own. | default: [openai/gpt-4.1-mini] | |
judge | list of string | Models tried in order after the judge’s model failed. | judge: [googleai/gemini-2.5-pro] | |
simulator | list of string | Models tried in order after the simulator’s model failed. | simulator: [anthropic/claude-sonnet-4-6] | |
browser_agent | list of string | Models tried in order after the browser agent’s model failed. | browser_agent: [googleai/gemini-2.5-flash] | |
scenario_generator | list of string | Models tried in order after the scenario generator’s model failed. | scenario_generator: [googleai/gemini-2.5-flash] | |
embedder | list of string | Embedding models tried in order after embedder failed; the default list does not apply to embeddings. | embedder: [openai/text-embedding-3-small] |
Example:
fallback:
default: [openai/gpt-4.1-mini]
judge: [googleai/gemini-2.5-pro]
simulator: [anthropic/claude-sonnet-4-6]
ModelRetry
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
attempts | integer | Tries per model on a rate limit or an outage or a timeout before the next fallback; default 3 and 1 retries nothing. | attempts: 3 | |
backoff | Duration | The wait before the second try; default 500ms and doubled every try with jitter. | backoff: 500ms | |
max_backoff | Duration | The longest wait between tries; default 10s. | max_backoff: 10s |
Example:
retry: { attempts: 3, backoff: 500ms, max_backoff: 10s }
Push
Either a string (true listens on a free local port), or an object with:
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
enabled | boolean | false turns push off; default true in the mapping form. | enabled: true | |
addr | string | Address to listen on such as 0.0.0.0:8099; default a free port on 127.0.0.1. | addr: 0.0.0.0:8099 | |
url | string | The URL the agent calls (a tunnel or a container address); default http:// | url: https://tamesu.tunnel.example.com |
Quarantine
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
reason | string | yes | Why the test is quarantined; usually the ticket that tracks it. | reason: "SUP-412 flaky since the payments migration" |
until | string | The last day (YYYY-MM-DD) of the quarantine; from the next day the test counts again. Without it the quarantine lasts until removed. | until: 2026-12-31 |
Example:
quarantine: { reason: "SUP-412 flaky since the payments migration", until: 2026-12-31 }
RESTListen
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
addr | string | Address to listen on such as 127.0.0.1:8089; default a free port on 127.0.0.1. The URL renders as {{listen.url}}. | addr: "127.0.0.1:8089" | |
path | string | The requests that carry replies: an optional method then a path with * wildcards such as POST /bot*/sendMessage; a trailing / matches the prefix. Default every request. Other requests get reply but are not read. | path: "POST /bot*/sendMessage" | |
session | string | JSONPath into the incoming payload that holds the session id ($.chat_id); routes each request to its session when tests run in parallel. Default the oldest open session. | session: $.chat_id | |
reply | any | The JSON tamesu answers the agent with (templates allowed); default {ok: true}. | reply: { ok: true, result: { message_id: 1 } } | |
status | integer | The status answered; default 200. | status: 200 | |
timeout | Duration | How long to wait for the agent’s request before the turn fails; default 60s. | timeout: 30s | |
idle | Duration | Keep reading after the first reply until nothing new arrives for this long (an agent that answers in several messages); unset completes the turn on the first request with content. | idle: 1s |
RESTOpen
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
url | string | yes | The request URL (templates allowed). | url: "https://directline.botframework.com/v3/directline/conversations" |
method | string | HTTP method; defaults to POST. | method: POST | |
headers | map of string to string | Request headers (templates allowed). | headers: { Authorization: "Bearer {{secrets.DIRECT_LINE_SECRET}}" } | |
body | any | Body template; none by default. | body: { user: { id: "{{session}}" } } | |
capture | map of string to string | JSONPaths into the JSON reply by name; {{session.NAME}} renders each value. | capture: { id: $.conversationId, token: $.token } |
RESTPoll
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
url | string | yes | The endpoint to poll; {{session.NAME}} and {{sent.NAME}} captures and {{cursor}} render. | url: "https://slack.com/api/conversations.replies?channel={{vars.channel}}&ts={{sent.ts}}" |
method | string | HTTP method; defaults to GET. | method: GET | |
headers | map of string to string | Request headers (templates allowed). | headers: { Authorization: "Bearer {{secrets.SLACK_USER_TOKEN}}" } | |
body | any | Body template; none by default. | body: { conversation: "{{session.id}}" } | |
interval | Duration | Time between polls; default 500ms. | interval: 1s | |
timeout | Duration | How long to wait for a reply before the turn fails; default 60s. | timeout: 30s | |
idle | Duration | Keep polling after the first reply until nothing new arrives for this long (an agent that answers in several messages); unset completes the turn on the first reply. | idle: 3s | |
cursor | string | JSONPath into each poll reply whose value renders as {{cursor}} in the next poll (a watermark). With it the polls accumulate; without it each poll replaces the last (the endpoint returns the whole thread). | cursor: $.watermark |
RedTeam
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
target | string | yes | The target the attacks talk to (a key of targets). | target: api |
agent | string | yes | What the agent is and what it must never do; the attacks pursue goals written against it. | agent: "Shop support for orders and returns; never more than 30-day returns, never other customers' data" |
playbooks | list of string | The built-in playbooks to run (tamesu playbooks lists them); every playbook when empty. | playbooks: [prompt_injection, pii_extraction, policy_drift] | |
max_turns | integer | Turns each attack gets (default 8). | max_turns: 8 | |
repeat | integer | Times each attack runs. | repeat: 3 | |
language | string | The attacker’s language (BCP 47). | language: es | |
tags | list of string | Extra tags on every generated test (red-team and playbook: | tags: [nightly] | |
vars | map of string to string | Values for {{vars. | vars: { shop: Mochi Shoes } | |
assert | list of Assertion | Assertions added to every generated test (a pack or a not_contains with the forbidden outcome). | assert: [ { not_contains: "90 days" } ] |
Example:
red_team:
target: api
agent: "Mochi Shoes support: never more than 30-day returns, no other customers' data, no refunds without verification"
playbooks: [prompt_injection, pii_extraction, policy_drift]
max_turns: 8
assert:
- not_contains: "90 days"
RemoteAgent
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
addr | string | Address to listen on such as 127.0.0.1:8095 when the agent under test is configured by hand; default a free port that {{remote. | addr: 127.0.0.1:8095 | |
name | string | The card’s name; default the key. | name: Billing | |
description | string | The card’s description. | description: Refunds and invoices | |
skills | list of RemoteSkill | The card’s skills: ids or objects with id / name / description / tags. | skills: [refund, { id: invoice-lookup, name: Invoice lookup }] | |
reply | string | What the agent answers every message that no replies entry matches. | reply: "I can help with refunds and invoices." | |
replies | list of RemoteReply | Scripted answers: the first entry whose when matches the message wins. | replies: [{ when: "(?i)refund", say: "Refund of 42 euros issued, reference RF-889" }] | |
forward | string | A real agent’s JSON-RPC URL: the remote agent becomes a recording proxy in front of it and every message and reply is recorded (templates allowed). | forward: https://billing.example.com/a2a | |
headers | map of string to string | Headers added to forwarded requests such as Authorization (templates allowed). | headers: { Authorization: "Bearer {{secrets.BILLING_TOKEN}}" } | |
extended | RemoteExtended | What the authenticated extended card adds (agent/getAuthenticatedExtendedCard): more skills and a fuller description for callers that send credentials. | extended: { skills: [invoice-export], description: "Refunds, invoices and exports for partners" } | |
token | string | A bearer token every request must carry (templates allowed); the card declares the scheme and a request without it gets 401. | token: "{{secrets.BILLING_AGENT_TOKEN}}" |
RemoteExtended
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
description | string | The description on the extended card; default the card’s. | description: "Refunds, invoices and exports for partners" | |
skills | list of RemoteSkill | Skills only the extended card lists. | skills: [invoice-export, { id: bulk-refund, name: Bulk refund }] |
RemoteReply
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
when | string | A regular expression on the message; omitted matches every message. | when: "(?i)cancel" | |
say | string | The agent’s words. | say: "Which order should I cancel?" | |
data | object (any keys) | A data part sent with the words. | data: { reference: RF-889 } | |
state | one of completed, input-required, failed | The task’s state after the reply; default completed. | state: input-required |
RemoteSkill
Either a string (The skill id), or an object with:
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
id | string | yes | The skill id. | id: refund |
name | string | The skill’s name; default the id. | name: Refunds | |
description | string | What the skill does. | description: Issues refunds for delivered orders | |
tags | list of string | Tags on the skill. | tags: [billing] |
ScriptFile
Either a string (A path relative to the suite, or a URI), or an object with:
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
path | string | A file relative to the suite; its bytes are sent. | path: fixtures/invoice.pdf | |
uri | string | A URI the agent fetches itself; sent instead of bytes. | uri: https://cdn.example.com/photo.jpg | |
name | string | The file name the agent sees; default the path’s base name. | name: invoice.pdf | |
mime | string | The media type; default from the extension. | mime: application/pdf |
Similar
Either a string (The text some agent turn should mean; the configured embedding model and a 0.8 minimum are the defaults), or an object with:
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
text | string | yes | What an agent turn should mean. | text: "Returning after 40 days is not possible" |
min | number | Minimum cosine similarity between the embeddings (0 to 1); default 0.8. | min: 0.7 | |
model | string | Embedding model (a Genkit reference); defaults to models.embedder. | model: googleai/text-embedding-004 |
Example:
- similar_to: "The return window is 30 days from delivery"
- similar_to: { text: "Returning after 40 days is not possible", min: 0.7, model: googleai/text-embedding-004 }
StateCommand
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
run | string | yes | The command (sh -c); templates render with the run’s vars and captured values and secrets. | run: "psql -tAc \"select status from orders where id = '{{captured.order_id}}'\"" |
exit | integer | The expected exit code; 0 when omitted. | exit: 0 | |
contains | string | Text the output must contain. | contains: refunded | |
regex | string | A regular expression the output must match. | regex: "refund(ed|ing)" | |
timeout | Duration | How long the command may take; default 30s. | timeout: 20s |
Example:
command: { run: "psql -tAc \"select status from orders where id = '{{captured.order_id}}'\"", contains: refunded }
StateHTTP
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
url | string | yes | The request URL; templates render with the run’s vars and captured values and secrets. | url: "{{vars.admin_url}}/orders/{{captured.order_id}}" |
method | string | GET when empty. | method: GET | |
headers | map of string to string | Request headers (templates allowed) | headers: { Authorization: "Bearer {{secrets.ADMIN_TOKEN}}" } | |
body | any | A string is sent as is; a mapping as JSON. | body: { reason: "test {{env.TAMESU_TEST}}" } | |
status | integer | The expected status code; any code under 400 when omitted. | status: 200 | |
json_path | JSONPathAssertion | A path that must exist in the JSON body and with equals have that value. | json_path: { path: $.status, equals: refunded } | |
contains | string | Text the body must contain. | contains: refunded | |
regex | string | A regular expression the body must match. | regex: '"type":"refund_issued"' | |
timeout | Duration | How long the request may take; default 30s. | timeout: 10s |
Example:
http: { url: "{{vars.admin_url}}/orders/{{captured.order_id}}", headers: { Authorization: "Bearer {{secrets.ADMIN_TOKEN}}" }, json_path: { path: "$.status", equals: refunded } }
VoiceAudio
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
encoding | one of pcm16, mulaw, alaw | The wire encoding; default pcm16. | encoding: mulaw | |
sample_rate | integer | The sample rate in Hz; default 16000 for pcm16 and 8000 for mulaw / alaw. | sample_rate: 8000 | |
frames | one of binary, json | binary (default): one chunk per binary websocket frame; json: the chunk travels base64-encoded inside a JSON message (send and receive say where). | frames: json | |
send | any | frames json: the message template per outgoing chunk; {{audio}} is the base64 chunk. | send: { user_audio_chunk: "{{audio}}" } | |
receive | string | frames json: JSONPath of the base64 audio in an incoming message; evaluated against the message as a one-element list such as $[*].audio_event.audio_base_64. | receive: "$[*].audio_event.audio_base_64" |
VoiceMessages
| Key | Type | Required | Description | Example |
|---|---|---|---|---|
open | any | A message template sent once after connecting. | open: { type: start, call_id: "{{session.call_id}}" } | |
transcript | string | JSONPath of the agent’s words in a message (its final transcripts); evaluated against the message as a one-element list such as $[?(@.type==‘transcript’ && @.role==‘assistant’)].transcript. Without it the audio is transcribed. | transcript: "$[?(@.type=='transcript' && @.role=='assistant' && @.transcriptType=='final')].transcript" | |
done | string | JSONPath that is truthy on the message that ends the call. | done: "$[?(@.type=='status-update' && @.status=='ended')].status" | |
interrupted | string | JSONPath that is truthy on the message that says the agent stopped for the caller. | interrupted: "$[?(@.type=='user-interrupted')].type" | |
tool_calls | string | JSONPath of the tool calls in a message: names or objects with name / args (or function.name / function.arguments). | tool_calls: "$[?(@.type=='tool-calls')].toolCallList[*]" | |
dtmf | any | A message template per keypad digit; {{digit}} renders. Without it a dtmf step is recorded as unsupported. | dtmf: { type: dtmf, digit: "{{digit}}" } | |
close | any | A message template sent before the connection is closed (a hangup). | close: { type: hangup } |