From 0c30b778fbd0716c0fbaf6587ceb1e77f63c257b Mon Sep 17 00:00:00 2001 From: Ziiii <273036393+zixuanguo786-ctrl@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:42:07 +0800 Subject: [PATCH] Preserve continuous rollout hard scores Rollout hard scores can be continuous when smoothed rewards are used. Converting the field through int() turned values like 0.75 into 0, which loses signal before scoring and serialization. Constraint: Keep the existing RolloutResult dictionary shape unchanged. Rejected: Clamp hard scores to 0 or 1 | contradicts existing continuous-score support in compute_score and sleep replay types. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Treat hard as numeric reward data, not only a binary label. Tested: uv run --with pytest pytest tests/test_types.py tests/test_scoring.py -q Tested: uv run --with ruff ruff check skillopt/types.py tests/test_types.py Not-tested: Full benchmark rollouts. --- skillopt/types.py | 10 +++++----- tests/test_types.py | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/skillopt/types.py b/skillopt/types.py index 9c23edb4..674a0a67 100644 --- a/skillopt/types.py +++ b/skillopt/types.py @@ -11,12 +11,12 @@ """ from __future__ import annotations -from dataclasses import dataclass, field, fields as dc_fields +from dataclasses import dataclass, field +from dataclasses import fields as dc_fields from typing import Any, Literal -from skillopt.evaluation.gate import GateAction, GateResult # noqa: F401 from skillopt.datasets.base import BatchSpec # noqa: F401 - +from skillopt.evaluation.gate import GateAction, GateResult # noqa: F401 # ── Atomic types ───────────────────────────────────────────────────────── @@ -109,7 +109,7 @@ class RolloutResult: """ id: str - hard: int + hard: float soft: float n_turns: int = 0 fail_reason: str = "" @@ -142,7 +142,7 @@ def from_dict(cls, d: dict) -> RolloutResult: extras = {k: v for k, v in d.items() if k not in known} return cls( id=str(d.get("id", "")), - hard=int(d.get("hard", 0)), + hard=float(d.get("hard", 0)), soft=float(d.get("soft", 0.0)), n_turns=int(d.get("n_turns", 0)), fail_reason=str(d.get("fail_reason", "")), diff --git a/tests/test_types.py b/tests/test_types.py index f39c8f62..b31d9e4d 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -3,8 +3,7 @@ import pytest -from skillopt.types import Edit, Patch - +from skillopt.types import Edit, Patch, RolloutResult # ── Edit ──────────────────────────────────────────────────────────────────── @@ -247,3 +246,17 @@ def test_nested_edit_from_dict_handles_dicts(self) -> None: assert isinstance(p.edits[0], Edit) assert p.edits[0].op == "append" assert p.edits[0].content == "hello" + + +# ── RolloutResult ──────────────────────────────────────────────────────────── + + +class TestRolloutResultRoundTrip: + """RolloutResult.to_dict() / RolloutResult.from_dict() round-trip.""" + + def test_preserves_fractional_hard_score(self) -> None: + """Hard can be a continuous reward and must not be truncated.""" + result = RolloutResult.from_dict({"id": "episode-1", "hard": 0.75, "soft": 0.5}) + + assert result.hard == pytest.approx(0.75) + assert result.to_dict()["hard"] == pytest.approx(0.75)