Saint Jerome by Jusepe de Ribera

Artwork: Saint Jerome by Jusepe de Ribera. Cleveland Museum of Art · Public domain

Evaluation

Skill Evaluation with Microsoft Waza

Before a reusable AI skill ships to a customer engagement, you want proof it does the right thing: fires on the prompts it should, stays quiet on the ones it shouldn’t, and produces relevant output. We standardized that proof on Microsoft Waza, a YAML-first evaluation framework, and wired it into a two-tier pipeline that runs in CI.

Why Waza over a custom framework

An earlier project used a bespoke MLflow-based evaluation harness. It worked, but it meant a lot of custom scorer code tightly coupled to the Databricks/MLflow stack. Waza replaces that with a standard, portable format:

The payoff: one evaluation format shared across every skill, scaffolded from the skill’s own SKILL.md so setup is minimal.

The eval spec

A Waza eval is a YAML file declaring the skill under test, config, metrics, graders, and tasks:

name: conversational-assistant-eval
description: Tests that the skill triggers on technician-assistant requests,
  stays silent on unrelated prompts, and produces domain-relevant output.
skill: conversational-assistant
config:
  trials_per_task: 1
  executor: mock
  model: claude-sonnet-4.6
metrics:
  - name: accuracy
    weight: 0.6
    threshold: 0.8
    description: Correctness of trigger routing and output relevance
  - name: robustness
    weight: 0.4
    threshold: 0.7
    description: Consistent behavior across edge cases and negative triggers
graders:
  - type: behavior
    name: efficiency
    config:
      max_tokens: 50000
tasks:
  - "tasks/*.yaml"

Each task is its own file, which keeps the suite readable and lets you add a case per failure (exactly the eval-driven loop). A positive-trigger task looks like this:

id: positive-trigger-001
name: Build Technician Briefing Agent
inputs:
  prompt: Build a conversational assistant that briefs field technicians before
    each job with equipment history, contract terms, and IoT alerts
expected:
  output_contains: [briefing, technician]
graders:
  - type: trigger
    name: should-trigger
    config:
      skill_path: .github/skills/field-service/conversational-assistant
      mode: positive
  - type: text
    name: keyword-match
    config:
      regex_match: ["(?i)agent|equipment|contract"]

Grader types

Waza combines several grader kinds so you can assert on different aspects of behavior:

Grader Checks
trigger Did the skill activate (positive) or stay silent (negative) for the prompt?
text Regex match / not-match on output — keyword coverage, absence of error strings
behavior Resource behavior such as token budget (max_tokens)

Negative-trigger tasks are as important as positive ones: a skill that fires on everything is as broken as one that never fires. The suite deliberately includes unrelated prompts and asserts the skill stays quiet.

Two tiers: skill evals and implementation evals

Skill-level evals prove routing and relevance. They don’t prove the generated code is any good. So a runner connects two tiers:

Hand-drawn two-tier stack showing Waza skill evaluations and implementation evaluations feeding one aggregate CI gate.

The runner orchestrates the end-to-end pipeline: discover implementation eval configs, load golden datasets and scorers, execute runs (mock or live), aggregate across implementations, and report a pass / warn / fail status.

@dataclass
class PipelineResult:
    eval_results: list[EvalResult] = field(default_factory=list)
    passed: int = 0
    warned: int = 0
    failed: int = 0

    @property
    def all_passed(self) -> bool:
        return self.failed == 0

That all_passed property is what a CI gate keys off: any failing eval fails the build.

Mock first, live when it matters

The mock executor runs the whole suite without API calls or cloud dependencies, which makes local iteration and CI fast and free. The copilot-sdk executor runs the same specs against real models for pre-deployment validation and cross-model comparison. Same YAML, two backends.

The day-to-day loop is three commands:

waza check   .github/skills/field-service/conversational-assistant   # validate structure/frontmatter
waza scaffold .github/skills/field-service/conversational-assistant  # generate an eval suite from SKILL.md
waza run     evals/conversational-assistant/eval.yaml --executor mock # run it

Because the same spec runs against multiple models, cross-model comparison is one flag away — and it’s how customers pick a model per deployment rather than by reputation:

Model Accuracy Robustness Avg tokens Verdict
claude-sonnet-4.6 0.94 0.88 12,400 Pass
gpt-4o 0.91 0.83 15,900 Pass
gpt-4o-mini 0.79 0.71 9,200 Below accuracy threshold (0.8)

Gating CI

The all_passed property maps straight onto a GitHub Actions gate — mock executor, no secrets, runs on every PR:

name: skill-evals
on: [pull_request]
jobs:
  waza:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: go install github.com/microsoft/waza/cmd/waza@latest
      - run: waza run evals/**/eval.yaml --executor mock --fail-on any

The trade-offs

We accepted these because a standardized, scaffoldable, CI-friendly format across all skills is worth more than a bespoke harness that only one team understands.

Takeaways

#evaluation#waza#skills#ci#ai-agents