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
1821Idempotency key contract
1922------------------------
2528insertion order or collection type would then change call identity and corrupt
2629redundancy / 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+
2838Metric 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
0 commit comments