Models, tools and agents
A prompt and its tools against a model, an MCP server through the model that uses it, and an orchestrator with the agents it delegates to.
Each recipe is a whole project you can copy, change the URLs and run; see Recipes for the other pages.
A prompt and its tools against a model, grounded and under failure
No agent server yet: the system prompt and its tool definitions are tested directly against a
model. The tools answer from the results in the suite, so the test knows exactly what the model
was told; grounded checks it said nothing beyond that, and fuzz_tools with cases runs the
same conversation once per failure mode to see the prompt cope.
# tamesu.yaml
version: 1
suites: ["prompt.yaml"]
genkit:
plugins: [googlegenai, anthropic]
models:
default: googleai/gemini-2.5-flash # the model the prompt runs on
simulator: googleai/gemini-2.5-flash
judge: anthropic/claude-sonnet-4-6
budgets: { max_cost_usd: 2 }
secrets:
GEMINI_API_KEY: env:GEMINI_API_KEY
ANTHROPIC_API_KEY: env:ANTHROPIC_API_KEY
suite: support-prompt
tags: [prompt]
targets:
prompt:
type: model
system_file: prompts/support.md # the prompt under test, next to the suite
fallback: [googleai/gemini-2.5-pro] # when the default model keeps failing
tools:
- name: lookup_policy
description: The store policy for a topic
parameters: { type: object, properties: { topic: { type: string } }, required: [topic] }
result: { topic: "{{args.topic}}", window_days: 30, exceptions: none, voucher_after: "10%" }
- name: lookup_order
description: An order by id
parameters: { type: object, properties: { id: { type: string } }, required: [id] }
result: { id: "{{args.id}}", status: delivered, delivered_days_ago: 40 }
- name: end_session
description: Call when the customer has no more questions
tests:
- name: return after 40 days
target: prompt
persona: { name: Marta, style: "polite, slightly confused" }
scenario:
goal: "Find out if order ORD-1002, delivered 40 days ago, can still be returned"
max_turns: 6
assert:
- tool_called: { name: lookup_policy, args: { topic: returns } }
- tool_sequence: { calls: [lookup_order, lookup_policy], exact: false }
- grounded: { context: { tool: lookup_policy } } # nothing the policy did not say
- judge: "The agent states the 30-day window, offers the voucher and promises no exception"
- not_contains: "As an AI"
- name: the tools misbehave
target: prompt
cases: # one test per failure mode
- { name: error, vars: { mode: error } }
- { name: empty, vars: { mode: empty } }
- { name: hostile, vars: { mode: hostile } }
- { name: slow, vars: { mode: delay } }
fuzz_tools:
lookup_order: "{{vars.mode}}"
persona: { name: Ken, style: "in a hurry" }
scenario: { goal: "Ask where order ORD-1001 is", max_turns: 4 }
assert:
- pack: graceful_degradation # no invented data, no raw error, a next step
- not_contains: "ORD-1001 has shipped"
- metric: { name: turn_latency_ms_max, lt: 60000 }
tamesu run --tags prompt # 5 tests: the policy question and four failure modes
tamesu run --matrix models.default=googleai/gemini-2.5-flash,openai/gpt-4.1-mini # the same suite on two models, compared
delay holds the normal answer back (8 seconds by default when fuzz_tools names only the mode;
{ mode: delay, delay: 8s } sets it); the other modes replace the answer. Every fuzzed call is a
tool_fuzz event in result.json.
An MCP server, through the model that uses it
The server is the thing under test: its tools are offered to a model and every call runs on the
server, so the suite sees the real descriptions, schemas, answers and errors. allow_tools
narrows what the model gets; tamesu coverage lists the tools that were never called.
# tamesu.yaml
version: 1
suites: ["mcp.yaml"]
genkit: { plugins: [googlegenai] }
models: { default: googleai/gemini-2.5-flash, judge: googleai/gemini-2.5-pro }
secrets:
GEMINI_API_KEY: env:GEMINI_API_KEY
MCP_TOKEN: env:MCP_TOKEN
suite: orders-mcp
targets:
local:
type: mcp
command: ./bin/orders-mcp # a stdio server, relative to the suite
args: ["--data", "fixtures/orders.json"]
env: { LOG_LEVEL: warn }
system: "You are the order desk. Use the tools for every fact; say so when a tool fails."
done_tool: end_session
hosted:
type: mcp
url: https://mcp.example.com/mcp # a streamable HTTP server
headers: { Authorization: "Bearer {{secrets.MCP_TOKEN}}" }
allow_tools: [lookup_order, cancel_order]
call_timeout: 20s
tests:
- name: the server offers its tools
target: local
scenario:
script: [ { say: "Hello, are you there?" } ]
assert:
- metric: { name: mcp_tools, gte: 3 }
- name: where is my order
target: local
persona: { name: Marta, style: polite }
scenario: { goal: "Find out where order ORD-1001 is and when it arrives", max_turns: 4 }
assert:
- tool_called: { name: lookup_order, schema: { type: object, required: [id], properties: { id: { pattern: "^ORD-" } } } }
- grounded: { context: { tool: lookup_order } }
- judge: "The agent gives the carrier and the delivery estimate the tool returned"
- name: an unknown order is reported, not invented
target: hosted
persona: { name: Ken, style: insistent }
scenario: { goal: "Ask about order ORD-9999, which does not exist", max_turns: 4 }
assert:
- tool_called: lookup_order
- pack: graceful_degradation
- not_regex: '(?i)ORD-9999 (has|was) (shipped|delivered)'
go build -o bin/orders-mcp ./cmd/orders-mcp
tamesu run # spawns the stdio server per session; the hosted one is reached over HTTP
tamesu coverage .tamesu/runs/<latest> # which tools the model called, and which never
A server error (isError) reaches the model as {"error": ...} and is a tool_error event; the
mcp_tools metric is how many tools the server listed.
An orchestrator and the agents it delegates to
An orchestrator that hands work to other agents over A2A is tested without those agents: the
suite declares them as remote_agents, tamesu runs them with scripted replies, the orchestrator
is pointed at their URLs, and the assertions say which agent must have been asked and what the
user must hear. A second test makes the billing agent fail and checks the orchestrator copes.
suite: orchestrator
tags: [a2a, orchestration]
vars: { orchestrator: "${ORCHESTRATOR_URL:-http://127.0.0.1:8100/a2a}" }
remote_agents:
billing:
description: Refunds and invoices
skills: [refund, invoice-lookup]
reply: "I can help with refunds and invoices."
replies:
- { when: "(?i)refund.*ORD-1001", say: "Refund of 42 euros issued, reference RF-889", data: { reference: RF-889 } }
- { when: "(?i)refund", say: "Which order should I refund?", state: input-required }
shipping:
addr: 127.0.0.1:8096 # the orchestrator reads SHIPPING_AGENT_URL=http://127.0.0.1:8096 at startup
skills: [track]
reply: "ORD-1001 is with the carrier and arrives tomorrow."
targets:
orchestrator:
type: a2a
url: "{{vars.orchestrator}}"
metadata: { billing_agent_url: "{{remote.billing.url}}" } # an orchestrator that takes its agents per call
tests:
- name: a refund goes to billing, not shipping
target: orchestrator
persona: { name: Marta, style: "direct, knows her order number" }
scenario: { goal: "Get a refund for order ORD-1001, delivered damaged", max_turns: 4 }
assert:
- delegated_to: { agent: billing, contains: "ORD-1001" }
- tool_not_called: shipping
- contains: "RF-889"
- judge: "The agent tells the user the refund was issued and gives the reference"
- metric: { name: delegations, lte: 2 }
- name: billing is down
target: orchestrator
fuzz_tools: { billing: error }
persona: { name: Marta, style: direct }
scenario: { goal: "Get a refund for order ORD-1001", max_turns: 4 }
assert:
- delegated_to: billing
- pack: graceful_degradation
- not_contains: "RF-889"
Run the orchestrator so it reads its agents’ URLs from its configuration, then
tamesu run --parallel 1 (the shipping agent has a fixed port). Each delegation is in the
transcript’s tool calls with the message and the reply, in the delegated events, and in
tamesu coverage under the tools, so a missing delegation reads as plainly as a missing tool
call. To test against the real billing agent while still recording and fuzzing it, replace its
replies with forward: https://billing.internal/a2a and a headers entry for its token.