Skip to content

NaN in a list-valued Score crashes the eval at sample-logging time #4491

Description

@MattFisher

Summary

A scorer that returns Score(value=[float("nan"), 1.0]) raises an error in the log-event validation step. After all samples complete, log-event validation fails and the eval finishes with status="error" and no results, reporting an opaque wall of pydantic errors (every event type in the union rejected). The identical value shaped as a dict ({"a": float("nan"), "b": 1.0}) works fine.

Found while stress-testing NaN handling around #4458/#4286. #3796 established NaN-at-root as the unscored sentinel for scalar, dict, and list-valued scorers — this is the one NaN placement that slips through: inside a list.

Reproduction

from inspect_ai import Task, eval
from inspect_ai.dataset import Sample
from inspect_ai.scorer import Score, Target, accuracy, scorer
from inspect_ai.solver import solver

@solver
def passthrough():
    async def solve(state, generate):
        state.output.completion = "done"
        return state
    return solve

@scorer(metrics=[accuracy()])
def listy():
    async def score(state, target):
        return Score(value=[float("nan"), 1.0])
    return score

log = eval(
    Task(dataset=[Sample(input="q", target="x")], solver=passthrough(), scorer=listy()),
    model="mockllm/model",
    display="none",
)[0]
print(log.status)   # "error"
print(log.results)  # None
print(log.error.message.splitlines()[0])
# 75 validation errors for list[union[SampleInitEvent,SampleLimitEvent,...]]

Reproduced on 0.3.232; the type definitions involved are unchanged on current main.

Root cause

The failure point (from log.error.traceback) is sample logging, not scoring:

_eval/task/run.py             log_sample
log/_recorders/streaming.py   materialize_streaming_sample
event/_validate.py            validate_events        # TypeAdapter(list[Event]).validate_python

Three facts compose:

  1. Pydantic's JSON serialization dumps NaN as null (same for root, dict, and list placements — verified: ScoreEvent(score=Score(value=[nan, 1.0]), ...).model_dump_json() contains "value":[null,1.0]). The sample's events pass through this serialization on their way into the streaming sample buffer.
  2. Value's branches are asymmetric (scorer/_metric.py): Mapping[str, str | int | float | bool | None] admits None, but Sequence[str | int | float | bool] does not.
  3. When the completed sample is materialized from the buffer for logging, validate_events re-validates the buffered event dicts. The ScoreEvent now carries "value": [null, 1.0], the Sequence branch rejects None, and because validation tries the dict against all 23 event types in the union, the single bad event reports as 75 unrelated-looking errors. The dict shape survives the same round-trip ({"a": null} validates against the Mapping branch), which is why only the list placement crashes.

The trap for scorer authors: Score(value=[None, 1.0]) fails fast at construction with a clear message, but NaN is a perfectly valid float at construction time — it only becomes an invalid null at the serialization boundary, so the failure is deferred to end-of-run and the diagnostic points nowhere near the scorer.

Suggested fix

The minimal, symmetric fix is to align the Sequence branch with the Mapping branch: Sequence[str | int | float | bool | None]. That makes the round-trip lossy-but-stable (NaN → null), matching what the dict shape already does.

If NaN-in-container is instead considered invalid (as distinct from the sanctioned NaN-at-root sentinel from #3796), the alternative is to reject it at Score construction with a targeted error message — anything is better than the current deferred union-validation dump with loss of the whole run's results.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions