Skip to content

Evaluations

Run a dataset through the same agent used by your application. Evaluations are how you know a prompt change made things better instead of just different.

Datasets

const evaluators = [_]zigai.evals.Evaluator{
    zigai.evals.containsExpected(),
};

var report = try (zigai.evals.Dataset{
    .cases = &.{.{
        .name = "weather",
        .prompt = "What is the weather in Madrid?",
        .expected_output = "sunny",
    }},
    .evaluators = &evaluators,
}).run(allocator, agent);
defer report.deinit();

Built-ins cover exact matches, substring checks, and valid JSON. Evaluator is a small callback interface for application-specific deterministic checks. ModelGrader wraps another agent, requests a typed pass/score/reason result, and validates that its score is between zero and one.

Repetitions, retries, and concurrency

Use runWithOptions when an experiment needs repeated runs or retries:

var report = try dataset.runWithOptions(allocator, agent, .{
    .repetitions = 3,
    .max_concurrency = 4,
    .io = io,
    .task_retry = .{ .max_attempts = 3 },
    .evaluator_retry = .{ .max_attempts = 2 },
    .hooks = hooks,
});
defer report.deinit();

Task and evaluator retry budgets are independent. A retry classifier can limit which errors are transient, while beforeRetryFn can apply application-owned backoff. Lifecycle hooks receive stable zero-based case indices, one-based repetition and attempt numbers, and every start, error, retry outcome, and end. The plain run method remains a single-run, single-attempt evaluation.

When max_concurrency is greater than one, ZigAI admits at most that many case runs through the supplied std.Io runtime, writes them into stable source-order slots, and aggregates usage after all work is joined. The model, evaluators, retry callbacks, and lifecycle hooks must be thread-safe in this mode. ZigAI serializes access to the caller allocator and the report arena.

Report and trace evaluators

Dataset-level report_evaluators run after every case and repetition has finished. They receive a read-only ReportView and append named scalar analyses with an optional assertion, unit, and reason. Report.summary, caseSummary, and scoreStatistics expose pass rates plus finite-score minimum, maximum, mean, and population standard deviation. A failed aggregate assertion makes Report.passed() false.

trace_evaluators inspect the same telemetry.Span values sent to the application exporter. Enabling them requires Agent.telemetry. ZigAI installs a per-case exporter tee, deep-copies span names, attributes, IDs, timing, and status into CaseResult.spans, and forwards spans and metrics unchanged. Trace evaluators use the ordinary evaluator retry policy and add results to the same ordered evaluation list after output evaluators.

Online evaluation

For production sampling, OnlineEvalSamplingPolicy makes a stable decision from the OpenTelemetry trace ID:

const policy = zigai.OnlineEvalSamplingPolicy{
    .trace_ratio = .{ .numerator = 1, .denominator = 100 },
};
if (try policy.includes(trace_context.trace_id)) {
    // Submit a trace-correlated OnlineEvalObservation to your evaluator path.
}

Online observations distinguish successful and failed runs. Evaluators return bounded zero-to-one scores, and result sinks retain the originating trace context. Sampling is deterministic across processes using the same trace ID and policy.

OnlineEvalQueue owns sampled observations until they are processed:

var online = try zigai.OnlineEvalQueue.init(
    allocator,
    io,
    evaluators,
    result_sink,
    .{
        .sampling = policy,
        .max_pending = 128,
        .overflow = .drop_oldest,
        .metric_exporter = telemetry_exporter,
    },
);
defer online.deinit();

const agent = zigai.Agent{
    .model = model,
    .telemetry = telemetry,
    .online_evals = &online,
};

Agent.online_evals requires Agent.telemetry. Each invocation gets isolated producer state and the same trace context as its OpenTelemetry run. Successful and failed runs are sampled; paused runs create no terminal observation.

Admission never blocks the agent

Admission never invokes evaluators, result sinks, or metric exporters, and it never waits for the queue lock. The queue preallocates its slots, deep-copies bounded prompt, output, usage, and failure data, and drops on producer contention. stats and zigai.online_eval.dropped expose backpressure, allocation, contention, closure, and processing losses.

Persistence and comparison

Persist portable datasets and complete reports with zigai.eval_io:

const yaml = try zigai.eval_io.stringifyDatasetYaml(allocator, dataset);
defer allocator.free(yaml);

var loaded = try zigai.eval_io.parseDatasetYaml(allocator, yaml, registry);
defer loaded.deinit();

Dataset files store cases and evaluator names. Loading binds those names through an explicit EvaluatorRegistry; callbacks and non-default per-case RunOptions are never serialized. JSON and readable YAML report files retain usage, evaluations, analyses, and telemetry spans.

Compare a candidate report with a baseline using zigai.eval_compare. Comparisons preserve stable case/repetition order, classify evaluator and aggregate-analysis changes, calculate signed usage deltas, and expose regressionFree() for gating. stringifyCiJson emits deterministic, versioned JSON with a pass or fail conclusion for CI artifacts.