Writing Metrics
A metric scores one trial. Metrics are attached to each task (not once per run), so a suite can grade heterogeneous work — a Q&A task and a coding task can carry different scorers. Every metric, however simple or elaborate, implements the same small protocol.
The protocol
No base class — a metric is any object with these three members (a structural Metric protocol).
What compute_scores receives
input.candidate is the trial under evaluation:
input.row.data is a dict describing the task and trial:
So an outcome metric reads candidate.output_text and row.data["reference"]; a trajectory
metric reads candidate.evidence.
Declaring outputs
A metric declares its outputs up front; the runtime validates that compute_scores returns exactly
those names, each coercible to the declared type (a missing or undeclared output raises). Build specs
with the MetricOutputSpec factories:
A metric may emit several outputs — for example an efficiency metric returning both a boolean and a count:
Prefer continuous_score when you want a numeric mean in the run summary. A boolean output
reports per-trial pass/fail but does not aggregate to a numeric mean on its own (though it still
contributes as 0/1 to a view).
Returning a result
Return a MetricResult whose outputs match output_spec by name:
Reading evidence
candidate.evidence (a CandidateEvidence) holds named descriptors, each exposed through a typed
handle. trace() returns the runner’s primary view as an ATIFTraceHandle | OTLPTraceHandle union.
Always guard for missing evidence, then narrow on handle.format before using format-specific
methods:
OTLP exposes the unflattened resourceSpans tree; it does not provide a generic tool_calls() method.
It’s a responsibility of consumer to parse resourceSpans tree specific to OTLP exporter to extract the searched data.
Asking for one trace format
A runner may record the same trial in more than one encoding. Harbor registers both under
trace:atif and trace:otlp when the agent under test emits both. Pass format= to choose one
and get a narrowed handle back, with no runtime branch:
trace() with no format still returns whichever encoding the runner made primary. For Harbor
that is the OTLP trace when the agent emits one and the ATIF trace otherwise — there is no setting
to choose between them. A format= request raises KeyError when the trial carries no trace in
that encoding, so a metric that depends on one specific view fails loudly instead of silently
scoring nothing.
The Score by Component guide has a
complete, runnable trajectory metric; the SDK’s example_metrics.py (under
examples/run_agent_eval/) shows filesystem and trace metrics.
How outputs are reported
Each output aggregates in result.summary under the key <metric.type>.<output> (mean / min / max /
std-dev). To roll several outputs into one reported score, define a view on the task — see
Score by Component.