Chat and web agents

A REST agent gated for release, a web shop in the browser, the backend behind the transcript, and escalation.

Each recipe is a whole project you can copy, change the URLs and run; see Recipes for the other pages.

A support agent over REST, gated for release

The project: models, a budget, the baseline limits, a PII pack and SLOs on every test, a hook that resets the agent’s conversation store, and the GitHub reporter for pull requests.

# tamesu.yaml
version: 1
suites: ["tests/**/*.yaml"]
genkit:
  plugins: [googlegenai]
models:
  default: googleai/gemini-2.5-flash
  judge: googleai/gemini-2.5-pro
budgets: { max_cost_usd: 3 }
defaults:
  timeout: 2m
  retries: 1
  assert:
    - pack: pii_leakage
    - not_contains: "as an AI language model"
  slo:
    turn_latency_ms_p95: 3000
    time_to_first_token_ms_p95: 1500
    llm_cost_usd: 0.05
baseline:
  dir: .tamesu/baseline
  max_latency_increase_pct: 20
hooks:
  before_each:
    - http: { url: "${ADMIN_URL:-http://localhost:8080}/sessions/reset", method: POST }
reporters: [console, junit, github]
secrets:
  GEMINI_API_KEY: env:GEMINI_API_KEY
  API_TOKEN: env:API_TOKEN

The suite: one parameterised test over the return window, a scripted conversation that reuses a captured order id, a golden transcript, and a tool-order check.

suite: support
tags: [support]
vars: { base_url: "${AGENT_URL:-http://localhost:8080}" }
targets:
  api:
    type: rest
    url: "{{vars.base_url}}/chat"
    headers: { Authorization: "Bearer {{secrets.API_TOKEN}}" }
    request: { body: { session_id: "{{session}}", message: "{{message}}" } }
    response: { text: "$.reply", tool_calls: "$.tool_calls[*]", done: "$.end_session" }
tests:
  - name: return window
    target: api
    cases:
      - { name: late, vars: { days: "40", expect: "30 days" } }
      - { name: early, vars: { days: "10", expect: "30 days" } }
    persona: { name: Marta, style: polite, language: en }
    scenario: { goal: "Ask whether shoes bought {{vars.days}} days ago can be returned", max_turns: 4 }
    assert:
      - contains: "{{vars.expect}}"
      - judge: "The agent states the 30-day return policy and does not promise an exception"
      - tool_sequence: { calls: [lookup_policy] }

  - name: order status with a captured id
    target: api
    capture:
      - { name: order_id, regex: "order (ORD-[0-9]+)" }
    persona: { name: Ken, style: in a hurry }
    scenario:
      goal: "Place an order for blue sneakers in a 42, then ask when it arrives"
      max_turns: 6
      script:
        - say: "I'd like the blue sneakers in a 42, please"
        - say: "When does order {{captured.order_id}} arrive?"
    assert:
      - captured: { name: order_id, matches: "^ORD-" }
      - golden: { file: golden/order-status.json, mode: judge }
      - metric: { name: turn_latency_ms_p95, lt: 2500 }

Run it against the baseline on every pull request, and refresh the baseline from a green main:

tamesu run --baseline                     # exit 1 on a new failure or a slower p95
tamesu baseline save .tamesu/runs/<ts>    # after a green run on main

A web shop in the browser: mocks, accessibility, visual baseline

# tamesu.yaml
version: 1
suites: ["shop.yaml"]
genkit:
  plugins: [googlegenai]
models:
  default: googleai/gemini-2.5-flash
  browser_agent: googleai/gemini-2.5-pro
defaults: { max_goal_steps: 15 }
secrets:
  GEMINI_API_KEY: env:GEMINI_API_KEY
  TEST_PASSWORD: env:TEST_PASSWORD
suite: shop
targets:
  web:
    type: browser
    url: "${WEBAPP_URL:-http://localhost:8090}"
    browser:
      viewport: 1280x800
      recording: webm
      mocks:
        - { url: "**/api/recommendations", body: { items: [] } }
        - { url: "**/analytics.js", abort: true }
tests:
  - name: log in, search and add to cart
    target: web
    steps:
      - goal: "Log in as marta with the password {{secrets.TEST_PASSWORD}}"
      - expect: "The page greets Marta by name"
      - action: { a11y: { impact: serious } }
      - goal: "Search for blue sneakers and add the first result in size 42 to the cart"
      - action: { assert_text: { selector: "#cart-count", contains: "1" } }
      - action: { visual: { name: cart, threshold: 0.01, mask: ["#date", ".avatar"] } }
    assert:
      - judge: "The steps show a successful login and one item in the cart"
      - metric: { name: goal_steps, lt: 12 }

  - name: checkout survives a slow payment service
    target: web
    steps:
      - action: { mock: { url: "**/api/payment", method: POST, status: 502, delay_ms: 2000 } }
      - goal: "Open the cart and pay"
      - expect: "The page says payment is unavailable and offers to retry"

Verify the backend, not just the transcript

The agent said the refund is done; was it? capture takes the order id from the agent’s words, a hook resets the fixture before each run, and two state assertions ask the backend after the conversation: an HTTP check against the admin API and a shell check against the ledger.

# tamesu.yaml
version: 1
suites: ["refunds.yaml"]
genkit: { plugins: [googlegenai] }
models: { default: googleai/gemini-2.5-flash }
vars: { admin_url: http://localhost:8081 }
secrets:
  GEMINI_API_KEY: env:GEMINI_API_KEY
  ADMIN_TOKEN: env:ADMIN_TOKEN
hooks:
  before_each: [ { http: { url: "{{vars.admin_url}}/test/reset", method: POST, headers: { Authorization: "Bearer {{secrets.ADMIN_TOKEN}}" } } } ]
suite: refunds
targets:
  api:
    type: rest
    url: "${AGENT_URL:-http://localhost:8080/chat}"
    response: { text: "$.reply", tool_calls: "$.tool_calls[*]" }
tests:
  - name: a damaged pair is refunded
    target: api
    persona: { name: Marta, style: "polite, has her order number ready" }
    scenario:
      goal: "Get a refund for order ORD-1001: the shoes arrived with a torn sole"
      max_turns: 6
    capture:
      - { name: order_id, regex: "(ORD-[0-9]+)" }
    assert:
      - judge: "The agent confirms the refund and gives a timeline"
      - tool_called: { name: issue_refund, args: { order_id: ORD-1001 } }
      - http:
          url: "{{vars.admin_url}}/orders/{{captured.order_id}}"
          headers: { Authorization: "Bearer {{secrets.ADMIN_TOKEN}}" }
          json_path: { path: "$.status", equals: refunded }
      - http:
          url: "{{vars.admin_url}}/orders/{{captured.order_id}}/events"
          regex: '"type":"refund_issued"'
      - command: { run: "./scripts/ledger.sh {{captured.order_id}}", contains: credited, timeout: 20s }

  - name: a refund outside the window is not issued
    target: api
    persona: { name: Ken, style: insistent }
    scenario: { goal: "Get a refund for order ORD-1002, delivered 40 days ago", max_turns: 6 }
    capture:
      - { name: order_id, regex: "(ORD-[0-9]+)" }
    assert:
      - judge: "The agent refuses the refund, explains the 30-day window and offers the voucher"
      - tool_not_called: issue_refund
      - http: { url: "{{vars.admin_url}}/orders/{{captured.order_id}}", json_path: { path: "$.status", equals: delivered } }
ADMIN_TOKEN=... tamesu run

The state checks run once the conversation ends; their result quotes the response or the output as evidence, and {{captured.order_id}} is whatever the agent actually said, so a wrong order id fails the right way.

Escalation: when the agent must hand off, and when it must not

The target says how this agent escalates (a tool call here; json_path or regex for agents that signal it otherwise), the tests say when it should and should not, and discloses_ai makes sure it never claims to be a person while doing it.

suite: escalation
tags: [handoff]
targets:
  api:
    type: rest
    url: "${AGENT_URL:-http://localhost:8080/chat}"
    response: { text: "$.reply", tool_calls: "$.tool_calls[*]", done: "$.end_session" }
    handoff: { tool: transfer_to_human, to: department, summary: summary }
tests:
  - name: an angry customer reaches a person quickly
    target: api
    persona: { name: Ray, style: "furious, swears every turn, repeats himself" }
    scenario: { goal: "Complain about a parcel that is two weeks late 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 and what was promised" }
      - pack: discloses_ai
      - metric: { name: handoff_turn, lte: 3 }

  - name: asking for a human is honoured
    target: api
    scenario:
      script: [ { say: "I want to talk to a real person, not a bot." } ]
    assert:
      - handoff: true
      - pack: discloses_ai

  - name: a simple question stays with the agent
    target: api
    persona: { name: Marta, style: polite }
    scenario: { goal: "Ask how long shipping takes", max_turns: 3 }
    assert:
      - handoff: false
      - contains: "business days"

  - name: no handoff for things the agent can do
    target: api
    persona: { name: Ken, style: "a little impatient" }
    scenario: { goal: "Change the delivery address of order ORD-1001 to 5 Rue Vert, Lyon", max_turns: 5 }
    assert:
      - handoff: false
      - tool_called: update_address
tamesu run --tags handoff
tamesu coverage .tamesu/runs/<latest>          # stop reasons: how many conversations ended in a handoff

The conversation stops when the signal fires, with stop reason handoff; a SIP transfer (transferred_to) counts as a handoff to its target, so the same assertions work on a phone line.