Skip to content

fix(eval): persist plan-execute token usage for cost/tokens reporting - #500

Open
AmirF194 wants to merge 1 commit into
IBM:mainfrom
AmirF194:fix/499-plan-execute-token-usage-not-persisted
Open

fix(eval): persist plan-execute token usage for cost/tokens reporting#500
AmirF194 wants to merge 1 commit into
IBM:mainfrom
AmirF194:fix/499-plan-execute-token-usage-not-persisted

Conversation

@AmirF194

@AmirF194 AmirF194 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Description

PlanExecuteRunner meters real token usage via _TokenMeter and writes accurate totals onto the OTel span (gen_ai.usage.input_tokens/output_tokens), but that data never reaches the persisted trajectory. persist_trajectory() only receives trajectory=list[StepResult], and StepResult has no token field, so _from_plan_execute() in src/evaluation/metrics.py always builds OpsMetrics with tokens_in=tokens_out=0, and _estimate_cost(model, 0, 0) short-circuits to None. Every offline evaluation report shows zero tokens and no cost for the plan-execute agent specifically; every SDK-based runner reports correctly because their Trajectory dataclass carries per-turn token fields.

Fix Details

Thread the meter's totals through as two new optional keyword arguments on persist_trajectory() (tokens_in, tokens_out), written onto the record only when given, so the six other runners' calls and their persisted shape are unchanged. PlanExecuteRunner.run() now passes self._meter.input_tokens/output_tokens. metrics_from_trajectory() reads them back off the record (getattr(..., None) or 0, so older persisted files without these keys still parse) and passes them into _from_plan_execute(), which now populates OpsMetrics.tokens_in/tokens_out and the cost estimate instead of hardcoding zeros.

Impact on Benchmarking

  • Baseline change: tokens_in, tokens_out and est_cost_usd for plan-execute scenario results move from always 0/None to the run's real usage. aggregate_ops()'s existing sums (tokens_in_total, tokens_out_total, est_cost_usd_total) are the only readers of these fields elsewhere in the codebase, and they already treat a None cost as excluded, so no other code path assumed the old zeros.

Related Issues

Verification Steps

  • New regression tests (test_orchestrator_persists_token_usage_alongside_trajectory, test_persist_includes_run_level_tokens_when_given, test_plan_execute_list_trajectory_reads_run_level_tokens) fail against unmodified main and pass on this branch; ran in a clean python:3.12-slim container with uv sync, __file__-checked against /repo/src/....
  • Full suite for the three touched modules (src/agent/tests/, src/evaluation/tests/, src/observability/tests/): 187 passed, 8 pre-existing failures unrelated to this change (missing google.protobuf in test_file_exporter.py, an unrelated scorer issue in test_static_json_scorer.py), same 8 failing identically on unmodified main.
  • coverage run/report confirms every changed line in the three touched files is covered.
  • Not checked: no live AGENT_TRAJECTORY_DIR/evaluation-CLI run against a real LLM backend; the repro is the unit/integration test suite above, not a manual end-to-end run.

Checklist

  • I have added tests that prove my fix is effective.
  • My code follows the project's Ruff formatting and linting rules. (repo has no Ruff config or dependency; nothing to run)
  • I have signed off my commits (DCO).

PlanExecuteRunner already meters real token usage via _TokenMeter and
writes it onto the OTel span, but never passed it to
persist_trajectory(). StepResult has no token field, so
_from_plan_execute() in metrics.py always built OpsMetrics with
tokens_in=tokens_out=0, and _estimate_cost short-circuited to None.
Every plan-execute evaluation report showed zero tokens and no cost,
while every SDK-based runner reported correctly because their
Trajectory dataclass carries per-turn token fields.

Thread the meter's totals through persist_trajectory() as optional
keyword arguments (omitted from the record when not given, so other
runners' persisted shape is unchanged), and have metrics_from_trajectory()
read them back into _from_plan_execute() for tokens_in/tokens_out and
the cost estimate.

Fixes IBM#499

Signed-off-by: Amir Fathi <amirfathi.me@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plan-execute runner's metered token usage never reaches persisted trajectories, so evaluation reports 0 tokens / no cost

1 participant