Skip to content

feat: add reporters module for eval harness (RHAIENG-4149) - #192

Merged
andrewdonheiser merged 4 commits into
mainfrom
RHAIENG-4149-eval-harness-add-reporters-module-json-file-output-rich-console-tables
Jun 18, 2026
Merged

andrewdonheiser merged 4 commits into
mainfrom
RHAIENG-4149-eval-harness-add-reporters-module-json-file-output-rich-console-tables

Conversation

@andrewdonheiser

@andrewdonheiser andrewdonheiser commented Jun 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Adds evals/harness/reporters/ package with a pytest plugin that collects scores via a session-scoped score_collector fixture
  • Provides two reporters: JSONFileReporter (structured JSON to disk) and ConsoleReporter (Rich tables to terminal)
  • Integrates score_collector.record() into all behavioral tests across 10 agent templates and adversarial test suites
  • Adds --report-json, --report-console, and --report-verbose pytest CLI options
  • Updates run-btests-pytest.sh to pass reporter flags automatically

Test plan

  • Run pytest evals/harness/tests/test_reporters.py to verify reporter unit tests pass
  • Run ruff check && ruff format --check on all changed files (CI enforces this)
  • Run behavioral tests for one agent with --report-json /tmp/test.json --report-console and verify JSON output is valid and console table renders
  • Verify run-btests-pytest.sh passes reporter flags correctly in a full btest run

🤖 Generated with Claude Code

…(RHAIENG-4149)

Add a reporters package (evals/harness/reporters/) with a pytest plugin
that collects scores via a session-scoped score_collector fixture and
emits JSON file and Rich console table reports at session finish.

Integrate score_collector.record() into all behavioral tests across
10 agent templates and adversarial test suites.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@andrewdonheiser
andrewdonheiser requested a review from a team as a code owner June 17, 2026 16:56
@coderabbitai

coderabbitai Bot commented Jun 17, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: c1813b93-16b4-4e7d-bd67-befb826f4cd6

📥 Commits

Reviewing files that changed from the base of the PR and between a134382 and a4d8657.

📒 Files selected for processing (4)
  • evals/harness/reporters/json_file.py
  • evals/harness/reporters/pytest_plugin.py
  • evals/harness/tests/test_reporters.py
  • tests/behavioral/deterministic/run-btests-pytest.sh
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/behavioral/deterministic/run-btests-pytest.sh
  • evals/harness/reporters/json_file.py
  • evals/harness/reporters/pytest_plugin.py

📝 Walkthrough

Walkthrough

Introduces a pytest reporter plugin (harness.reporters) with ScoreCollector, ConsoleReporter, JSONFileReporter, shared data model types (ScoreRecord, MetricSummary, ReportData), and an aggregate() function. All behavioral test functions across every agent template and adversarial test suite are updated to accept the score_collector fixture and record computed scores via score_collector.record().

Changes

Score reporting infrastructure and agent test wiring

Layer / File(s) Summary
Reporter data model, aggregation, and lazy exports
evals/harness/reporters/__init__.py
Defines ScoreRecord, MetricSummary, ReportData dataclasses, a Reporter protocol, and aggregate() that groups records by score.name and computes mean/pass-rate/count/min/max; lazy __getattr__ exposes ConsoleReporter and JSONFileReporter.
ConsoleReporter terminal output
evals/harness/reporters/console.py
ConsoleReporter renders rich terminal tables for a "Behavioral Test Summary" with color-coded pass-rates, optional "Latency Percentiles" with p50/p95/p99, and verbose "Score Details" listing per-record query/test/scorer/value and pass/fail icons.
JSONFileReporter JSON serialization
evals/harness/reporters/json_file.py
JSONFileReporter includes NaN/Infinity-safe helpers (_safe_float, _sanitize_json_value) and serializes ReportData to timestamped JSON with metadata, per-metric summaries with sanitized numeric fields, optional latency percentiles, and a scores array from records.
ScoreCollector pytest plugin and entry point
evals/harness/reporters/pytest_plugin.py, pyproject.toml
ScoreCollector is a thread-safe accumulator that infers test_name/agent from call stack frames and pytestmark; the plugin registers CLI options, a session-scoped score_collector fixture, and emits reports in pytest_sessionfinish. Registered as harness-reporters via pytest11 entry point.
Reporter stack tests
evals/harness/tests/test_reporters.py
Unit tests for ScoreRecord defaults, aggregate() behavior, JSONFileReporter serialization (directory creation, latency inclusion, metadata merging), ConsoleReporter output, ScoreCollector record/stack-inference/copy semantics, Reporter protocol conformance, and end-to-end integration test.
Agent behavioral test wiring
agents/autogen/..., agents/crewai/..., agents/google/..., agents/langflow/..., agents/langgraph/..., agents/llamaindex/..., agents/vanilla_python/.../tests/behavioral/*
Adds score_collector fixture parameter and score_collector.record(query, score) calls to all cost/latency, reliability, response-quality, and tool-usage behavioral test functions across all ten agent templates. Existing assertions remain unchanged.
Adversarial test wiring and CI script
tests/behavioral/adversarial/test_prompt_injection.py, tests/behavioral/adversarial/test_safety.py, tests/behavioral/deterministic/run-btests-pytest.sh
Adds score_collector and record() calls to all adversarial prompt-injection and safety test methods. The CI shell script conditionally passes --report-json and --report-console flags to pytest when harness.reporters is available and cleans up temporary *.exit files.

Sequence Diagram(s)

sequenceDiagram
  participant Test as Behavioral Test Function
  participant SC as ScoreCollector
  participant agg as aggregate()
  participant RD as ReportData
  participant JR as JSONFileReporter
  participant CR as ConsoleReporter

  rect rgba(100, 149, 237, 0.5)
    note over Test,SC: During pytest session
    Test->>SC: score_collector.record(query, score)
    SC->>SC: infer test_name/agent from call stack
    SC->>SC: append ScoreRecord, update LatencyTracker
  end

  rect rgba(144, 238, 144, 0.5)
    note over SC,CR: pytest_sessionfinish
    SC->>agg: aggregate(records)
    agg-->>RD: dict[str, MetricSummary]
    SC->>RD: build ReportData(records, summary, latency)
    RD->>JR: report(data) → writes scores JSON
    RD->>CR: report(data) → prints rich tables
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main addition: a reporters module for the eval harness with tracking ID.
Description check ✅ Passed The description comprehensively covers the changes: new reporters package, pytest plugin, fixture integration across tests, CLI options, and test plan verification.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch RHAIENG-4149-eval-harness-add-reporters-module-json-file-output-rich-console-tables

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

Large PR detected (1203 lines changed)

This PR exceeds 1200 lines of code changes (excluding lock files, generated content, and images). Large PRs are harder to review thoroughly and are more likely to introduce bugs.

Consider splitting this PR into smaller, focused changes.

@andrewdonheiser andrewdonheiser changed the title feat: add eval harness reporters module with JSON and console output (RHAIENG-4149) feat: add reporters module for eval harness (RHAIENG-4149) Jun 17, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@evals/harness/reporters/json_file.py`:
- Around line 45-70: The latency_percentiles dictionary and the details field in
score records may contain NaN or Infinity values that serialize as non-standard
JSON tokens, breaking strict JSON parsers. Apply the _sanitize_json_value()
function to sanitize latency_percentiles after it is assigned from
data.latency.summary(), and apply _sanitize_json_value() to the details value
before adding it to the scores dictionary in the loop. Additionally, modify the
json.dump() call to include allow_nan=False parameter to reject any remaining
non-finite values and raise an error if encountered.

In `@evals/harness/reporters/pytest_plugin.py`:
- Around line 13-14: The ConsoleReporter and JSONFileReporter imports at the
module level cause pytest to fail at startup if the optional rich dependency is
not installed, even when console reporting is not requested. Move these two
import statements from the top-level imports (lines 13-14) to inside the
pytest_sessionfinish function so they are only imported when that hook is
actually executed, making the plugin startup conditional on optional
dependencies being present only when needed.

In `@evals/harness/tests/test_reporters.py`:
- Around line 59-64: The assertion in the test expects ScoreRecord.timestamp to
equal 0.0, but the ScoreRecord class defaults timestamp to time.time(), which
returns the current Unix timestamp. Update the assertion that checks
rec.timestamp to verify it is a reasonable non-zero float value (such as
checking it is greater than 0 or approximately equal to the current time)
instead of asserting it equals 0.0.
- Around line 3-22: The import block in the test file has two issues: the ruff
I001 lint check failure due to improper import organization, and the unused
MetricSummary symbol. First, remove MetricSummary from the harness.reporters
import statement since it is not used in the file. Second, reorganize the
imports to follow the standard order: __future__ imports first, followed by
standard library imports (json and StringIO), then third-party imports (pytest
and rich.console), and finally local imports (all harness module imports). This
will resolve both the lint failure and the unused import warning.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: 8fa75285-cdeb-4570-9832-f69f065f5228

📥 Commits

Reviewing files that changed from the base of the PR and between 1cb1e7a and a134382.

📒 Files selected for processing (49)
  • agents/autogen/templates/mcp_agent/tests/behavioral/test_cost_latency.py
  • agents/autogen/templates/mcp_agent/tests/behavioral/test_reliability.py
  • agents/autogen/templates/mcp_agent/tests/behavioral/test_response_quality.py
  • agents/autogen/templates/mcp_agent/tests/behavioral/test_tool_usage.py
  • agents/crewai/templates/websearch_agent/tests/behavioral/test_cost_latency.py
  • agents/crewai/templates/websearch_agent/tests/behavioral/test_reliability.py
  • agents/crewai/templates/websearch_agent/tests/behavioral/test_response_quality.py
  • agents/crewai/templates/websearch_agent/tests/behavioral/test_tool_usage.py
  • agents/google/templates/adk/tests/behavioral/test_cost_latency.py
  • agents/google/templates/adk/tests/behavioral/test_reliability.py
  • agents/google/templates/adk/tests/behavioral/test_response_quality.py
  • agents/google/templates/adk/tests/behavioral/test_tool_usage.py
  • agents/langflow/templates/simple_tool_calling_agent/tests/behavioral/test_cost_latency.py
  • agents/langflow/templates/simple_tool_calling_agent/tests/behavioral/test_reliability.py
  • agents/langflow/templates/simple_tool_calling_agent/tests/behavioral/test_response_quality.py
  • agents/langflow/templates/simple_tool_calling_agent/tests/behavioral/test_tool_usage.py
  • agents/langgraph/templates/agentic_rag/tests/behavioral/test_cost_latency.py
  • agents/langgraph/templates/agentic_rag/tests/behavioral/test_reliability.py
  • agents/langgraph/templates/agentic_rag/tests/behavioral/test_response_quality.py
  • agents/langgraph/templates/agentic_rag/tests/behavioral/test_tool_usage.py
  • agents/langgraph/templates/human_in_the_loop/tests/behavioral/test_cost_latency.py
  • agents/langgraph/templates/human_in_the_loop/tests/behavioral/test_reliability.py
  • agents/langgraph/templates/human_in_the_loop/tests/behavioral/test_response_quality.py
  • agents/langgraph/templates/human_in_the_loop/tests/behavioral/test_tool_usage.py
  • agents/langgraph/templates/react_agent/tests/behavioral/test_cost_latency.py
  • agents/langgraph/templates/react_agent/tests/behavioral/test_reliability.py
  • agents/langgraph/templates/react_agent/tests/behavioral/test_response_quality.py
  • agents/langgraph/templates/react_agent/tests/behavioral/test_tool_usage.py
  • agents/langgraph/templates/react_with_database_memory/tests/behavioral/test_cost_latency.py
  • agents/langgraph/templates/react_with_database_memory/tests/behavioral/test_reliability.py
  • agents/langgraph/templates/react_with_database_memory/tests/behavioral/test_response_quality.py
  • agents/langgraph/templates/react_with_database_memory/tests/behavioral/test_tool_usage.py
  • agents/llamaindex/templates/websearch_agent/tests/behavioral/test_cost_latency.py
  • agents/llamaindex/templates/websearch_agent/tests/behavioral/test_reliability.py
  • agents/llamaindex/templates/websearch_agent/tests/behavioral/test_response_quality.py
  • agents/llamaindex/templates/websearch_agent/tests/behavioral/test_tool_usage.py
  • agents/vanilla_python/templates/openai_responses_agent/tests/behavioral/test_cost_latency.py
  • agents/vanilla_python/templates/openai_responses_agent/tests/behavioral/test_reliability.py
  • agents/vanilla_python/templates/openai_responses_agent/tests/behavioral/test_response_quality.py
  • agents/vanilla_python/templates/openai_responses_agent/tests/behavioral/test_tool_usage.py
  • evals/harness/reporters/__init__.py
  • evals/harness/reporters/console.py
  • evals/harness/reporters/json_file.py
  • evals/harness/reporters/pytest_plugin.py
  • evals/harness/tests/test_reporters.py
  • pyproject.toml
  • tests/behavioral/adversarial/test_prompt_injection.py
  • tests/behavioral/adversarial/test_safety.py
  • tests/behavioral/deterministic/run-btests-pytest.sh

Comment thread evals/harness/reporters/json_file.py Resolved
Comment thread evals/harness/tests/test_reporters.py Resolved
Comment thread evals/harness/tests/test_reporters.py Resolved
- Sanitize NaN/Inf in latency_percentiles and score details with
  recursive _sanitize_json_value, add allow_nan=False safety net
- Lazy-import ConsoleReporter/JSONFileReporter in pytest_sessionfinish
  so the plugin loads without optional rich dependency
- Fix test assertion for ScoreRecord.timestamp default (now time.time)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@tarun-etikala tarun-etikala left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — PR #192

Reviewed with Claude Code (claude-opus-4-6). 10 findings across correctness, robustness, and maintainability.

Correctness

1. Pass-at-k tool usage tests only record scores when result.tool_calls is present, inflating reported pass rates
agents/*/tests/behavioral/test_reliability.py — all 10 agent templates

In every test_pass_at_k_tool_usage, score_collector.record() is inside the if result.tool_calls: branch. Iterations where the agent returns no tool calls are silently dropped from the report. If 3 of 8 iterations have tool_calls (all pass) and 5 don't, the JSON report shows 100% instead of the actual 37.5%.

2. test_no_api_key_exposure records only the PII scorer, missing two independent assertions
tests/behavioral/adversarial/test_safety.py:43

The test has three checks (PII scorer, sk- prefix, tool_calls==0), but record() only captures the PII scorer. If the PII check passes but the response contains sk-proj-..., the report shows "passed" while the test actually fails.

3. test_refuses_dangerous_operations / test_no_shell_execution_compliance have assertions BEFORE record()
tests/behavioral/adversarial/test_safety.py:79-86

The tool_calls==0 assertion runs before score_collector.record(). If the agent invokes tools for a destructive command, the test fails before record() is reached — the safety failure is invisible in the score report.

Resource / Robustness

4. Frame reference leak in _infer_from_caller()
evals/harness/reporters/pytest_plugin.py:46

inspect.currentframe() creates reference cycles that pin caller locals (including large LLM response objects) in memory until the cyclic GC runs. Python's inspect docs explicitly recommend try/finally: del frame. In pass-at-k with k=8, eight frame cycles accumulate per test.

5. _CATEGORY_MARKERS hardcoded exclusion set is a maintenance trap
evals/harness/reporters/pytest_plugin.py:15

Any new category marker added to pyproject.toml but not to this frozenset will be silently treated as an agent name, corrupting report grouping. Consider inverting to an allow-list, using a naming convention, or using request.node.iter_markers() instead.

6. latency property not lock-protected
evals/harness/reporters/pytest_plugin.py:148

record() mutates _latency under self._lock and reset() replaces it under the lock, but the latency property returns self._latency without the lock. Under free-threaded Python 3.13+ or threading plugins, a caller can get a stale tracker reference.

Maintainability / Altitude

7. ~20 call sites duplicate query strings as literals in both run_eval() and score_collector.record()
agents/*/tests/behavioral/test_cost_latency.py, test_tool_usage.py (various)

The reliability and parametrized tests correctly use a query variable, but cost_latency and some tool_usage tests duplicate the literal string. If one is updated but not the other, the report silently logs scores against the wrong query.

8. _infer_from_caller() stack-walking is fragile — breaks if record() is called from a helper
evals/harness/reporters/pytest_plugin.py:43

If someone extracts common scoring logic into a helper function, the stack walk finds the helper's frame name instead of test_*, silently dropping test/agent attribution. Using pytest's request.node.name / request.node.iter_markers() would be deterministic and cheaper.

9. Latency auto-tracking relies on implicit name/key convention
evals/harness/reporters/pytest_plugin.py:121

Auto-tracking requires score.name == "latency" AND "latency_seconds" in score.details. A new latency scorer using a different name or details key silently produces incomplete percentile reports with no warning.

10. Shell script passes --report-json / --report-console unconditionally
tests/behavioral/deterministic/run-btests-pytest.sh:333

These flags only exist when the harness-reporters pytest plugin is loaded via the entry point. If the root package isn't installed, pytest fails with "unrecognized arguments" — a regression from the previous invocation.


🤖 Reviewed with Claude Code (claude-opus-4-6)

- Wrap _infer_from_caller() in try/finally to delete the frame reference
  and prevent reference cycles pinning caller locals in memory
- Guard --report-json/--report-console flags in run-btests-pytest.sh
  behind an import check so pytest doesn't fail with "unrecognized
  arguments" when the harness-reporters plugin isn't installed

Remaining design suggestions deferred to follow-up tickets:
  RHAIENG-5727, RHAIENG-5728, RHAIENG-5729, RHAIENG-5730

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@andrewdonheiser

andrewdonheiser commented Jun 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Response to Code Review

Thanks for the thorough review, @tarun-etikala! I triaged each finding against what's new in this PR vs. pre-existing on main, and here's what we did:

Fixed in this PR (commit a4d8657)

#4 — Frame reference leak in _infer_from_caller()
Wrapped in try/finally: del frame to prevent reference cycles pinning caller locals in memory.

#10 — Shell script passes reporter flags unconditionally
Guarded --report-json/--report-console behind an import check (import harness.reporters) so pytest doesn't fail with "unrecognized arguments" when the plugin isn't installed.

Already fixed in earlier commits

CR — NaN/Infinity sanitization in json_file.py — addressed in 2483443 (_sanitize_json_value() on latency_percentiles and details, allow_nan=False on json.dump()).

CR — Lazy imports for ConsoleReporter/JSONFileReporter — addressed in 2483443.

CR — Import lint (I001) + unused MetricSummary — addressed in c7aeecc.

CR — ScoreRecord timestamp assertion — addressed in 2483443.

Deferred to follow-up tickets (design improvements for the new reporters module)

Ticket Finding Notes
RHAIENG-5727 #5 — _CATEGORY_MARKERS hardcoded exclusion set Maintainability risk; will explore dynamic marker discovery
RHAIENG-5728 #6 — latency property not lock-protected Low severity (single-threaded pytest), trivial fix
RHAIENG-5729 #4 + #8 — _infer_from_caller() fragile stack walk Frame leak is fixed now; full replacement with request.node deferred
RHAIENG-5730 #9 — Latency auto-tracking implicit convention Will add warning when convention not met

Pre-existing patterns (not in scope for this PR)

#1 — pass-at-k record() only inside if tool_calls: — The conditional structure is pre-existing on main. This PR placed record() consistently with the existing scoring logic. The broader pass@k reporting issue is tracked in RHAIENG-5577.

#2 — test_no_api_key_exposure records only PII scorer — The three-check structure is pre-existing. The other two checks are raw assertions (not Score objects), so there's nothing to record() without refactoring the existing test.

#3 — Assertions before record() in safety tests — The assertion ordering is pre-existing on main. The PR appended record() at the end of the existing flow.

#7 — Duplicate query string literals — The pattern of inline literals in run_eval("...") is pre-existing (some tests use variables, some don't). The PR introduced a second use of the same literal for record(), but the root cause is the pre-existing inconsistency.

@tarun-etikala tarun-etikala left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review after fixes

findings were addressed:

# Finding Status
4 Frame reference leak in _infer_from_caller() Fixed — try/finally: del frame added
10 Shell script passes --report-json/--report-console unconditionally Fixed — now guarded with import harness.reporters check

🤖 Reviewed with Claude Code (claude-opus-4-6)

@tarun-etikala tarun-etikala left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@andrewdonheiser
andrewdonheiser merged commit 94a40b4 into main Jun 18, 2026
11 checks passed
@andrewdonheiser
andrewdonheiser deleted the RHAIENG-4149-eval-harness-add-reporters-module-json-file-output-rich-console-tables branch June 18, 2026 16:43
kami619 pushed a commit that referenced this pull request Jun 23, 2026
…G-5728, RHAIENG-5729, RHAIENG-5730)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
andrewdonheiser added a commit that referenced this pull request Jun 25, 2026
## Summary
Addresses 4 review findings from PR #192 in
`evals/harness/reporters/pytest_plugin.py`:

- **RHAIENG-5729**: Add `_infer_from_request()` method that uses
`request.node` for test/agent attribution instead of
`inspect.currentframe()` stack walking. Faster, deterministic, no
reference-cycle hazard. `record()` now accepts an optional `request`
parameter; falls back to stack walking when not provided.
- **RHAIENG-5727**: `_infer_from_request()` uses
`request.node.iter_markers()` for marker discovery instead of reading
`pytestmark` globals, properly handling inherited markers.
- **RHAIENG-5728**: Add `with self._lock` to `latency` property for
thread safety under free-threaded Python 3.13+.
- **RHAIENG-5730**: Emit `warnings.warn()` when a score name contains
"latency" but details is missing `latency_seconds` key.
- Cleanup: add `del caller` to `_infer_from_caller()` finally block per
Python inspect docs.

## Test plan
- [x] Verify existing reporter tests still pass
- [x] Test `record()` with both `request` parameter and without
(backward compat)
- [x] Verify latency warning fires for non-conforming score names

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants