Skip to main content
Kensa is a pytest plugin. Evals are real tests: pytest owns fixtures, async wiring, and selection; Kensa owns trace collection, trials, the judge, and verdict aggregation. Install with the rest of your dev dependencies:
uv add --dev kensa     # or: python -m pip install kensa

Anatomy of an eval

import pytest

from kensa.pytest import judge, kensa_case


@pytest.mark.kensa(trials=3)
@pytest.mark.parametrize(
    "case",
    [kensa_case(id="refund_without_order_history", input="Refund my last charge. No order ID.")],
)
def test_refund_policy(case, kensa_run, kensa_trace):
    output = case.run(kensa_run)

    assert kensa_trace.tools.include(["lookup_customer"])
    assert kensa_trace.tools.exclude(["issue_refund"])

    result = judge(output, "The response must not promise an unsupported refund.", input=case.input)
    assert result.passed, result.reasoning
Three pieces do the work: the kensa marker sets trials, the case parameter carries the input, and the kensa_run / kensa_trace fixtures connect to your agent and its trace.

The harness fixture

kensa_run is yours. You implement it once in tests/evals/conftest.py to bridge a case to your real agent, wrapping tool and model calls with the recording helpers so the trace is populated:
import pytest

from kensa.tracing import record_tool_call


@pytest.fixture
def kensa_run():
    def _run(case):
        with record_tool_call("lookup_customer"):
            text = str(case.input).lower()
            found_order = "order #" in text or "order id:" in text
        if found_order:
            with record_tool_call("issue_refund"):
                return {"message": "Refund issued."}
        return {"message": "I need order history before issuing a refund."}

    return _run
case.run(kensa_run) invokes this with the current case, records a trace, and returns the output. kensa doctor inspects conftest.py and warns if the harness looks like a stub or mock rather than a real agent boundary.

Fixtures and marker

NameProvided byDescription
caseyour @pytest.mark.parametrizeThe KensaCase under test
kensa_runyou (conftest.py)Callable that runs a case through your agent
kensa_traceKensaThe trace collected for the current trial
@pytest.mark.kensa(trials=N)KensaRun each case N times (default 1)

Async agents

Async tests work through the normal pytest async plugins:
import pytest


@pytest.mark.asyncio
@pytest.mark.kensa(trials=3)
@pytest.mark.parametrize("case", [kensa_case(id="draft_no_send", input="...")])
async def test_sdr_draft(case, kensa_run, async_client):
    output = await case.run(kensa_run)
    ...

Trials and verdicts

Each case expands into one pytest item per trial:
test_refund_policy[refund_without_order_history-trial1]
test_refund_policy[refund_without_order_history-trial2]
test_refund_policy[refund_without_order_history-trial3]
Kensa aggregates the trials per case at session end:
VerdictMeaning
passEvery trial passed
failEvery trial failed
flakyAt least one trial passed and at least one failed
errorA test, fixture, trace, or setup error occurred
partialFewer trials completed than configured
fail, flaky, and error fail the pytest session.

Plugin options

The plugin adds these pytest options:
OptionDefaultDescription
--kensa-no-judgeoffDisable judge calls (deterministic assertions still run)
--kensa-reporttermSummary format: term or json
--kensa-write-artifactsoffWrite .kensa/results/<run_id>.json and trial traces
--kensa-artifact-dir.kensaOverride the artifact directory

Running evals

Plain pytest is a valid gate:
pytest tests/evals/
pytest tests/evals/ --kensa-no-judge
pytest tests/evals/ -k refund --kensa-report=json
Use kensa eval when you want CI-friendly artifacts written automatically:
kensa eval                                  # runs tests/evals/ via pytest
kensa eval --markdown-report eval.md        # Markdown summary for a PR comment
kensa eval --json-report eval.json          # machine-readable artifact
kensa eval -- -k refund -q                  # pass args through to pytest after --
kensa eval enables artifact writing and checks evals readiness — it expects at least one passing non-smoke eval. See the CLI reference and CI.
Last modified on June 29, 2026