Skip to content

Diagnostic dataclasses use bare-str fields where Literal/enum belongs; reason constant duplicated across modules #531

Description

@bingran-you

Summary

Multiple diagnostic and result fields are declared as bare str even though they only ever take a small closed set of values, and one of those constants is duplicated in two source files with no shared source of truth. Static type checkers, IDEs, and consumers cannot rely on the closed-set property today.

Concrete instances

A. Diagnostic.reason is bare str

src/benchflow/diagnostics.py:89, 115, 142IdleTimeoutDiagnostic.reason: str = "idle_timeout", SandboxStartupDiagnostic.reason: str = "sandbox_startup_failed", TransportClosedDiagnostic.reason: str = "transport_closed".

These are dataclass-class invariants — there is exactly one valid value per class. Should be Literal["idle_timeout"] etc., or a shared DiagnosticReason = Literal["idle_timeout", "sandbox_startup_failed", "transport_closed", …] re-exported from the diagnostic module.

Today, IdleTimeoutDiagnostic(reason="not_actually_a_timeout") type-checks. That's a regression vector.

B. IDLE_TIMEOUT constant duplicated

src/benchflow/_utils/scoring.py:10 declares IDLE_TIMEOUT = "idle_timeout". src/benchflow/diagnostics.py:89 hardcodes the same string as the diagnostic's default. Renaming one side does not error.

C. usage_source / price_source are bare str

src/benchflow/metrics.py:47, models.py:138, rollout.py:452: usage_source: str = "unavailable". The only two values ever assigned in v0.5 are "provider_response" and "unavailable". Compare to TrajectorySource = Literal["acp", ...] in models.py:15 — same project, different rigor.

price_source is null today but typed the same way; tighten when its enum is decided.

D. Inconsistent to_dict() Nones-policy

TransportClosedDiagnostic.to_dict() (diagnostics.py:170-194) drops keys with None values. IdleTimeoutDiagnostic.to_dict() does NOT drop Nones — uses bare asdict(). Same registry, different serialization shape. Hides "field is absent" vs "field is null" inconsistently.

Severity

P2.

No current incorrect-output bug — these are type-safety / API-stability hygiene issues. But:

  • (A) and (B) together mean a one-line rename of "idle_timeout" in scoring.py will silently misclassify every future idle-timeout incident.
  • (C) means consumers cannot exhaustively pattern-match on these fields.
  • (D) means transport_error_info and idle_timeout_info look different to JSON-schema-driven consumers for no documented reason.

Proposed fix

  1. Introduce DiagnosticReason (or per-class Literal) and replace the bare-str defaults. Move IDLE_TIMEOUT / TRANSPORT_CLOSED / SANDBOX_STARTUP_FAILED constants into diagnostics.py and re-export from _utils/scoring.py.

  2. Typedef UsageSource = Literal["provider_response", "unavailable"] in metrics.py; apply to all three sites.

  3. Decide the to_dict() Nones policy (drop-or-keep) once and apply uniformly. Lean toward "drop None" to match TransportClosedDiagnostic since it's already in production.

  4. Add a unit test that asserts to_dict() outputs are JSON-schema-stable across all Diagnostic subclasses (no surprise key presence/absence).

Found via

v0.5 e2e validation §2, §3, §4+§5 at 5a7a326. Independently noted by three subagents inspecting result.json and diagnostics.py.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Anti-pattern / type safety / docs precision / minor schema drift / non-deterministic but contained.area:diagnosticsIssue / PR lives primarily in the "diagnostics" subsystem.enhancementNew feature or requestreviewingFix PR open and awaiting reviewstatus:in-progressHas assignee or linked draft PR.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    In Progress

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions