Skip to content

Commit d489684

Browse files
committed
fix(evals): materialize registry once per load, validate ids before dedupe, document summary identity limits
1 parent 5aeebb1 commit d489684

2 files changed

Lines changed: 43 additions & 7 deletions

File tree

evals/agent_trajectory/metrics.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
Codex App Server backend emits ``arguments_summary`` instead of
1111
``arguments``).
1212
13-
All functions in this module are pure: they consume plain data (a log list and
14-
a ``GoldenSample``) and never touch the filesystem, network, or LLM. This keeps
15-
the metrics layer deterministic, unit-testable without an API key, and free of
16-
``src/`` imports — it can score trajectories from any source.
13+
The scoring functions in this module are pure: ``compute_trajectory_metrics``,
14+
``format_text_report`` and ``validate_golden_sample`` consume plain data (a log
15+
list and a ``GoldenSample``) and never touch the filesystem, network, or LLM.
16+
The one exception is the loader ``load_golden_samples``, which reads the golden
17+
JSON file from disk. This keeps the metrics layer deterministic, unit-testable
18+
without an API key, and free of ``src/`` imports — it can score trajectories
19+
from any source.
1720
1821
Idempotency key contract
1922
------------------------
@@ -25,6 +28,13 @@
2528
insertion order or collection type would then change call identity and corrupt
2629
redundancy / retry counts.
2730
31+
Codex App Server entries carry only ``arguments_summary`` — the redacted and
32+
truncated preview produced by ``redact_diagnostic_value`` — so their identity
33+
is best-effort: distinct calls whose summaries collide after redaction or
34+
truncation may be over-counted as redundant. A stable argument fingerprint
35+
requires a producer-side change in ``src/agent/codex_agent_backend.py``, which
36+
is out of scope for this metrics layer.
37+
2838
Metric semantics
2939
----------------
3040
* ``redundant_calls``: every occurrence of a (tool, args-key) pair beyond its
@@ -273,6 +283,9 @@ def load_golden_samples(
273283
if not isinstance(data, list):
274284
raise ValueError(f"golden samples file must contain a JSON list, got {type(data).__name__}")
275285

286+
# Materialize once before the loop: a one-shot generator must survive the
287+
# validation of every sample, not just the first.
288+
known = set(known_tool_names) if known_tool_names is not None else None
276289
golden_fields = {f.name for f in fields(GoldenSample)}
277290
samples: List[GoldenSample] = []
278291
seen_ids: set = set()
@@ -283,12 +296,15 @@ def load_golden_samples(
283296
sample = GoldenSample(**{k: v for k, v in item.items() if k in golden_fields})
284297
except TypeError as exc:
285298
raise ValueError(f"sample #{index} has invalid fields: {exc}") from exc
299+
# Structural validation runs before duplicate detection so that a
300+
# mistyped (possibly unhashable) id is rejected as a ValueError here
301+
# instead of crashing the membership check below.
302+
issues = validate_golden_sample(sample, known)
303+
if issues:
304+
raise ValueError(f"sample '{sample.id}': " + "; ".join(issues))
286305
if sample.id in seen_ids:
287306
raise ValueError(f"duplicate sample id: {sample.id}")
288307
seen_ids.add(sample.id)
289-
issues = validate_golden_sample(sample, known_tool_names)
290-
if issues:
291-
raise ValueError(f"sample '{sample.id}': " + "; ".join(issues))
292308
samples.append(sample)
293309
return samples
294310

tests/test_agent_trajectory_metrics.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,12 @@ def test_registry_membership_with_one_shot_generator(self):
450450
known = (name for name in ["a", "b"])
451451
assert validate_golden_sample(sample, known) == []
452452

453+
def test_loader_materializes_registry_once_for_multiple_samples(self):
454+
# A one-shot generator must survive loading the whole checked-in file:
455+
# the first sample must not exhaust it for the remaining samples.
456+
samples = load_golden_samples(known_tool_names=(name for name in _repo_tool_names()))
457+
assert len(samples) == 3
458+
453459

454460
class TestLoadGoldenSamplesErrors:
455461
@staticmethod
@@ -552,6 +558,20 @@ def test_non_list_expected_tools_with_known_names_raises_valueerror(self, tmp_pa
552558
with pytest.raises(ValueError, match="expected_tools must be a list"):
553559
load_golden_samples(path=path, known_tool_names={"get_realtime_quote"})
554560

561+
def test_unhashable_id_raises_valueerror(self, tmp_path):
562+
# Structural validation must run before duplicate detection: an
563+
# unhashable id would otherwise crash the seen_ids membership check
564+
# with a TypeError instead of the documented ValueError.
565+
sample = {
566+
"id": ["x"],
567+
"task_description": "t",
568+
"stock_code": "600519",
569+
"expected_tools": ["get_realtime_quote"],
570+
}
571+
path = self._write_sample(tmp_path, [sample])
572+
with pytest.raises(ValueError, match="id must be a non-empty string"):
573+
load_golden_samples(path=path)
574+
555575
def test_non_bool_allow_optional_tools_raises(self, tmp_path):
556576
sample = {
557577
"id": "x",

0 commit comments

Comments
 (0)