Skip to content

feat(reporting): add quantitative skill performance dashboard - #179

Open
chrisknvidia wants to merge 46 commits into
mainfrom
feat/christopherk/issue-170-performance-dashboard
Open

chrisknvidia wants to merge 46 commits into
mainfrom
feat/christopherk/issue-170-performance-dashboard

Conversation

@chrisknvidia

@chrisknvidia chrisknvidia commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Closes #170.

Saved reports have to be inspected one at a time to compare agent skill quality and resource usage across models and harnesses. This PR adds an optional Python/Streamlit dashboard, skillevaluator dashboard, that loads canonical Tier 3 JSON, consolidated reports, local catalogs, or retained Tier 3 run directories. It shows:

  • Performance: one sortable row per run, agent (harness and model), recorded feature configuration, and condition. Each row has score, pass@k, tokens, agent execution time, and USD cost as totals and as per-trial means.
  • With vs without skill: paired with-minus-without differences (absolute and percent) for score, pass@k, tokens, time, and cost. A Comparison reason column says why a pair has no differences.
  • Filters and exports: filters for skill, model, harness, harness features, condition, and status. Both tables download as CSV.

Measurement rules:

  • Missing measurements stay blank and are never treated as zero.
  • Conditions pair only within their originating report and agent occurrence, and only with the same feature configuration, dataset digest, attempt policy, and matching successful trial coverage.
  • Repeated attempts pair by case, as main's HTML report does: each attempt needs its own trial ID.
  • Agent time comes from canonical agent_seconds (fix: target dependency coverage and Tier 3 result integrity #196) or from retained agent_execution, under the same timezone-aware rule main uses. Cost appears only when a retained run records agent_result.cost_usd.
  • Harness features and adverse conditions are import-only metadata. No evaluator command writes them today, and the dashboard tells the user so.

Loading and safety:

  • Loading is bounded. External or symlinked retained artifacts are rejected, and one bad input does not hide the others.
  • Inputs are read once per server and reused while you filter. Reload reports re-reads them from disk.
  • Browser uploads accept 16 MB (16,000,000 bytes); local JSON reads accept 16 MiB.
  • The launcher binds to 127.0.0.1 and starts Streamlit from its own package directory with python -P. It disables usage telemetry and the first-run email prompt, uses the next free port unless --port is given, and forwards SIGTERM/SIGHUP to Streamlit.

The dashboard reads saved outputs only. It never starts evaluations or estimates prices.

Changes

Area What changed
CLI skillevaluator dashboard [PATHS...] [--port N] [--no-browser] under Core workflows; dashboard optional extra (streamlit>=1.61,<2; 1.61 is the first release that honours the allowed-hosts setting)
Data reporting/dashboard_data.py: normalization, pairing, fail-closed totals, bounded loaders, row dedupe
UI reporting/dashboard_app.py (cached loading, filters, tables, CSV) and reporting/dashboard_ui/skillevaluator_dashboard.py (isolated Streamlit entry script)
Launcher reporting/dashboard.py
Docs Performance dashboard section in docs/reports.mdx; docs/cli-reference.mdx; dashboard row in the docs/installation.mdx extras table; CHANGELOG; third-party notices

Follow-up commits on this branch

After the original five commits, the branch was merged with main (latest d891b69) and got 18 follow-up commits from an enhancement pass, then 13 more that address a 14-finding inline review (all threads resolved), a fix for #77's compacted pruned-agent conditions, and a CI-only test fix. Their details are in the PR comments and review threads.

Verification

  • Signed-off commits; documentation, changelog and third-party notices updated.

  • Final head 2c0c013 (main d891b69 merged): all 17 CI checks pass, including Tests (Python 3.12 and 3.13), Tier 2 on Windows and macOS, DCO, Gitleaks and CodeQL. Earlier, at 4a945f8, two launcher tests failed only in the serial Python 3.12 coverage job, because they stubbed find_spec for all modules; 92e6219 stubs only the Streamlit lookup. Local full suite (Python 3.13, Streamlit 1.65) at 4a945f8: 13,090 passed, 22 skipped. There were 6 local failures, all outside the dashboard: 2 test_harbor_secure_docker_environment sidecar tests that fail the same way on main in this environment, and 4 bridge/preflight timing tests that pass on rerun. After the last merge, 5,554 reporting/CLI/evaluation/Tier 3 tests passed.

  • Dashboard and CLI gate tests/reporting tests/test_cli.py tests/golden tests/test_oss_packaging.py: passed. Includes producer-contract tests that run main's canonical exporter and check that the dashboard's totals and deltas match the report's resources for 1 and 3 attempts.

  • ruff check src tests, ruff format on the PR files, uv lock --check, git diff --check, and fern check (no errors; the authenticated redirect check is skipped without a Fern token).

  • Launcher behaviour checked live against Streamlit 1.65:

    • a streamlit.py in the current directory no longer runs;
    • a project .streamlit/config.toml can't open CORS;
    • no email prompt on first run;
    • the default launch moves to 8502 when 8501 is busy;
    • SIGTERM to the launcher stops the server.
  • Original author verification at 7b0448f:

    • 210 UI matrix cases;
    • 33 browser journeys across Chromium, Firefox and WebKit;
    • 24 CLI/runtime cases;
    • a retained real dual-arm evaluation checked against raw-artifact arithmetic;
    • CI green on Python 3.12 and 3.13.

    These were not all re-run after the follow-up commits; the automated suites above were.

  • No credentials, private datasets or proprietary benchmark content added.

No provider-backed agent evaluation or billing request ran. Physical mobile/Safari devices, assistive technology, and AppTest upload support on the Streamlit 1.61 floor were not tested; the upload tests skip if AppTest lacks file_uploader.

Deferred follow-ups

  • Adverse-vs-without comparisons, once an evaluator produces adverse arms.
  • Evaluator commands writing per-trial cost and dashboard_metadata.harness_features, which would make those columns populate without hand edits.
  • Aggregating comparable deltas across runs, and charts. [FEA]: create a tables and rows quantitative dashboard to compare agent skill performance #170 asks for tables.
  • Whether the browser upload limit (16 MB) and the local file limit (16 MiB) should be one value.

Outstanding review gate

The original author asked for dedicated Bugbot and Security Review passes, which were unavailable in their session. A multi-area review with adversarial verification has since run on this branch (see the review threads), but it does not replace those specific passes if they are still required.

Release Impact

  • Updated CHANGELOG.md

🤖 Generated with Claude Code

chrisknvidia and others added 9 commits October 4, 2026 23:08
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Conflicts resolved:
- CHANGELOG.md: kept every entry from both sides. The dashboard Added entry
  and the two dashboard Fixed entries move into main's new Unreleased
  section; the released 0.5.0 section stays as on main.
- src/skillevaluator/evaluation/tier3_report.py: took main. Main already
  stops reporting unmeasured token counters as zero (since 0.5.0), with
  bounds checks and multi-row trial handling; canonical trials omit the
  token record when prompt or completion is unavailable.
- src/skillevaluator/tier3/harbor/report_data.py: took main's
  _trajectory_token_counter, which returns None for missing or
  unrepresentable counters and None for missing steps.
- tests/reporting/test_report_data_bounds.py: kept main's loader and
  ATIF v1.7 tests and the PR's canonical-export token test. The PR test now
  expects main's representation (no token record when prompt or completion
  is unmeasured); its dashboard, native-recovery and HTML assertions are
  unchanged.
- uv.lock: took main's lock and re-ran uv lock for the PR's dashboard extra
  (streamlit>=1.49,<2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Main's canonical Tier 3 report now collapses reward rows that share one
physical trial into one logical trial and omits their token counters, and
the collector's num_trials counts logical trials (num_reward_rows counts the
rows). The dashboard found multi-step trials only through duplicate trial IDs
in canonical trials, so after the merge a retained multi-step run without
step_results reported the root mirror's tokens as the trial's usage.

Count shared reward trial IDs in the retained trial directories, require
authoritative step_results for those trials, and keep the ambiguity notice.
The test fixture now writes the collector's current summary shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
After the merge, canonical Tier 3 trials take main's representation: they
leave out the token record when prompt or completion usage was not measured
instead of writing null counters. Update the changelog entry and the
dashboard guide to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Resolve the CHANGELOG.md conflict with #203 by keeping both sides' Fixed entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
@rng1995
rng1995 marked this pull request as ready for review October 16, 2026 04:57
rng1995 and others added 17 commits October 15, 2026 22:07
Resolve the CHANGELOG.md conflict with #196 by keeping both sides' Fixed
entries. uv.lock merged without conflict and `uv lock --check` passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Resolve the CHANGELOG.md conflict with #190 by keeping both sides' Fixed
entries. `uv lock --check` passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
The Windows CI job decodes encoding-less read_text/write_text as cp1252, so
every dashboard test file read or write now passes encoding="utf-8".

AppTest's default_timeout is a wall-clock budget, and the first st.dataframe
in each xdist worker imports pandas and pyarrow inside that timed thread
(about 7 s cold against 1.5 s warm). Import them before AppTest and raise the
ceiling to 60 s; passing runs are not slower. A cold `-n 8` run of
tests/reporting had two AppTest failures that passed on the next run.

Also assert that `dashboard` is listed under Core workflows in --help.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
…ime rule

Canonical Tier 3 JSON records each single-step trial's agent execution time
as `agent_seconds`, but the dashboard only read a per-trial `duration_seconds`
that no producer writes. Agent time and its delta were therefore blank for
every uploaded, canonical, or validate report. Read `agent_seconds` first; an
invalid value stays unavailable rather than falling back, and the legacy
field is still read for hand-built payloads.

Retained runs now use Harbor's shared `agent_execution_seconds`, which treats
timestamps without a timezone as unavailable, so a run's dashboard time
matches what its own canonical JSON and HTML report record.

Label the column "Agent time" so it is not mistaken for wall-clock duration,
add a contract test that builds the canonical payload with the producer and
checks the dashboard against its `resources` block, and document the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
…eport

Harbor names each repeated attempt <task>__<random> without an attempt
ordinal, so the dashboard treated every --n-attempts run above 1 as
unpaired: scores and pass@k were blanked and every comparison was shown
as mismatched coverage. The HTML report pairs these runs by matching
case counts.

Unnumbered attempts now pair by case count when every attempt has its
own trial ID, which is the recorded identity of a physical attempt.
Missing or shared trial IDs, mixed numbered and unnumbered attempts,
and differing explicit attempt sets still fail closed. The dashboard no
longer guesses attempt numbers from trial names, which also misread
authored case IDs such as retry-attempt1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
… loader bound

The report loader caps each JSON artifact at 2 MiB, and Harbor trajectories
are bounded by node count, not bytes. One large with-skill trajectory recorded
an artifact_loading reason, and the dashboard then treated the whole report
as truncated trial coverage and blanked every score, pass@k and comparison.
The HTML report keeps those scores, because a trajectory feeds only per-trial
tokens and steps, and canonical trials then omit tokens so coverage counts
already keep token totals unavailable.

Treat trajectory-only loader reasons as telemetry. Any other artifact, a
malformed reason list, or a bare truncated marker still blocks comparisons,
and the separate trial-count check still catches a missing trial.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
…la escaping

Report notices can carry text taken from a shared report, such as agent
keys, upload names and catalog paths. Streamlit parses alert bodies as
Markdown, so a crafted key could render a remote image or a link. Each
notice now renders with st.text under one fixed warning, and the
expander counts unique notices instead of every duplicate.

The CSV export also missed full-width formula prefixes (for example
U+FF1D), which spreadsheets fold to "=". Text cells are now probed after
NFKC normalization, and tests cover the CR, LF and NBSP cases too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
…ports

Streamlit reruns the whole script on every widget change, so each filter
click reloaded every command-line path and re-decoded every upload. On a
large retained run that made a filter change take seconds.

Command-line inputs are now cached by path plus a cheap stat fingerprint,
so a rewritten file or a new entry directly inside a directory is picked
up on the next interaction. Deeper edits inside a directory, such as a
run still writing trials, appear after the new Reload reports button.
Uploads are cached by their widget file ID instead of hashing the bytes.
The size check now uses the upload's recorded size, and every message
and source label is unchanged.

Both CSV download buttons no longer trigger a rerun, and new AppTests
cover the upload path, including the 16 MB boundary and invalid JSON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
The dashboard skipped only inputs whose resolved paths were equal, so
natural invocations such as a results directory plus a run inside it, a
results directory plus its latest symlink, or a run plus its result.json
loaded the same rows twice under the same source. Every pair then read
as ambiguous duplicate conditions with blank deltas, and the condition
row count doubled. Uploading the same file twice doubled the rows and
the run count too.

Rows reloaded from the same source, report, agent and condition are now
kept once with a notice. That key is unique within one parse, so
identical embedded reports in one file are still separate rows, and a
CLI path and an upload of the same file keep their distinct sources.
Byte-identical uploads are skipped with a notice; the digest is computed
inside the cached upload parse, so it runs once per uploaded file.

The existing notice-count test loads a directory and a file inside it,
so it now also expects the overlap notice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
…eir own row

A declared trial count of about 2**64 or more passed the count check, and
when the trials could not be keyed it became the row's trial count. The
performance table then failed Arrow conversion, so one malformed report
blanked every valid report on the page. Counts above one million, far
beyond the 512-trial loader bound, are now treated as invalid coverage
counts, so that row shows the usual notice with no score or usage.

An unreadable trials directory in a retained run raised out of the run
loader and dropped every agent and condition in the run. That condition
now gets a notice and blank usage totals while the run's other rows load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
…un loader

The retained-run loader hard-coded 512 and 4096, the same trial bounds
the canonical report loader uses, with nothing tying the two together.
They are now named constants, and a test checks that they still match
report_data. A repeated directory check that could never trigger is
removed, the JSON reader stats each file once, the catalog and discovery
limit notices are written once, _set_usage always takes an explicit
expected count, and the multi-step ambiguity notice is matched exactly
rather than by a copied substring. Messages and behaviour are unchanged.

New tests cover the trial discovery limit without creating 513
directories, the local JSON size limit at and just past the boundary,
and that a resolved multi-step scope leaves no ambiguity notice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
"incomplete or mismatched coverage" covered several different causes, so
users had to read the JSON to learn why a pair had no deltas. A new
Comparison reason column, included in every comparison view and in the
CSV, names the first failing check: a failed or skipped arm, invalid
trial counts, truncated trial data, incomplete coverage in one arm,
different cases or attempts, a different dataset or attempt policy, a
missing arm, or an arm loaded more than once. comparison_status and the
pairing rules are unchanged.

The status filter also ran before pairing, so filtering to succeeded
rows turned a recorded failed baseline into "missing condition". Status
now narrows the performance table only, like the condition filter, and
the comparison shows the failed arm as the reason instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
When the filters excluded every row, the comparison tab still told users
to load reports with both conditions, although such reports were loaded.
It now says that no rows match the filters, as the performance tab does.
When pairs exist but none is comparable, an info line points to the
Comparison reason column. Coverage cells now read "1 trial" instead of
"1 trials".

No SkillEvaluator command writes dashboard_metadata.harness_features, so
every evaluator-produced run shows features as unknown. When no loaded
row records features, a caption says how to add them, and the reports
guide states that the field is added by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
The performance table showed only usage totals summed over each row's
trials, so a model or harness that ran more trials looked proportionally
more expensive. Issue #170 is about comparing models and harnesses, where
trial counts can differ.

Each row now also carries tokens, agent time, and cost per trial. A total
is published only when every expected trial is measured, so dividing it
by the expected trial count gives an exact mean, and the per-trial value
stays blank whenever the total does. The columns sit right after the
score in the table and CSV; the totals remain. Comparison deltas are
unchanged because pairs already require identical trial keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
… extra

skillevaluator is not published on PyPI, so the pip install command in the
missing-extra error and the reports guide could not succeed and invited
dependency confusion. Point both at the same uv tool install from GitHub
that the installation guide uses, with [all,dashboard] so a reinstall keeps
the evaluator extras. List the dashboard extra in the extras table and stop
saying [all] installs every extra.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
…ctory and prompts

The launcher ran python -m streamlit from the caller's directory, so a
streamlit.py there ran instead of Streamlit, a project .streamlit/config.toml
could reopen CORS, and the reporting package directory shadowed stdlib html.
Start Streamlit with -P from a dedicated dashboard_ui directory, restrict
WebSocket Host headers to loopback, and pin CORS and XSRF protection.

An explicit headless=false made Streamlit ask for an email and exit 255
without a TTY, so headless is now set only for --no-browser and the prompt
is off. The file watcher banner and developer toolbar are switched off too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
rng1995 and others added 3 commits October 16, 2026 10:26
The launcher always passed --server.port=8501. Streamlit treats a port on
the command line as manually set and exits with status 1 when it is busy,
so a second dashboard or any other Streamlit app on 8501 made the plain
command fail. Pass the port only when the user chose one, so Streamlit
moves to the next free port otherwise, and print a hint when an explicit
port fails. The --port option also gains help text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Ctrl+C reaches the whole process group, but a SIGTERM sent only to the
launcher, as an IDE stop button, a process supervisor, or kill PID does,
killed the launcher and left Streamlit running and holding the port.
Run Streamlit with Popen and forward SIGTERM and SIGHUP to it while
waiting, so the launcher stays alive to report the exit status and the
port hint. After Ctrl+C the launcher waits up to 10 seconds for Streamlit
to stop before terminating it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
The Unreleased Fixed entry for omitting unmeasured token usage describes
behaviour main shipped in v0.5.0 (#83), and this PR changes no producer; the
second Fixed entry describes the dashboard, which is itself new. Fold both into
one Added entry that names what the dashboard compares, where time and cost come
from, and how inputs are cached.

In the reports guide, describe a performance row and the pairing rule
precisely (same report and agent, dataset digest and attempt policy), drop the
advice to regenerate canonical JSON (no command does that), describe multi-step
trials as v0.5.0 canonical JSON writes them, document the agent_result token
fallback, and list the accepted harness_features forms and their precedence.
Two tests pin the precedence and the accepted forms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
@rng1995

rng1995 commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up enhancements and fixes (pushed to this branch)

@chrisknvidia: I merged main into the branch, then added 18 follow-up commits from a review and enhancement pass. Every commit is signed off. The full local suite passes (11,861 tests); the only 2 failures are sidecar tests that fail the same way on main in my environment. Head is caeffe0. Each item below is its own commit, so any of them can be reverted on its own.

Merges

Correctness

  • f31c9af: read canonical per-trial agent_seconds (added in fix: target dependency coverage and Tier 3 result integrity #196) as agent time.
    • Canonical trials never contain duration_seconds or cost_usd, so agent time and its delta were blank for every uploaded report.
    • The retained-run path now uses main's agent_execution_seconds, which treats timestamps without a timezone as unavailable, as the HTML report does.
    • A new producer-contract test runs main's exporter and checks that the dashboard's totals and deltas equal the report's resources.
  • a6084f0: pair repeated Harbor attempts by case.
    • With --n-attempts N, Harbor names trials <task>__<shortuuid>, so names carry no attempt number. _trial_keys therefore returned None, and every score, pass@k and comparison went blank.
    • Each attempt with a distinct trial ID now counts as recorded identity. This is the same "matching case counts" rule main's HTML report uses.
    • Still fail-closed: missing or empty trial IDs, a trial ID shared across cases, mixed explicit and implicit attempts, and differing explicit attempt sets.
    • Trial-name parsing (…attempt1) is gone, because it misread case IDs such as retry-attempt1.
    • This reverses your "require recorded identity" rule, so please check it.
  • dfa7288: a trajectory over the report loader's 2 MiB cap no longer blocks score comparisons.
    • Main keeps the score and only drops that trial's tokens.
    • Every other artifact_loading reason still blocks.
  • d3f1029: a count of 2**64 or more crashed Arrow conversion and blanked the whole page. Counts are now capped at 1,000,000 and go through the invalid-coverage path. An unreadable trials/ directory now fails only its own row instead of dropping the whole run.

Performance

  • 4a15034: cache inputs across Streamlit reruns.
    • Measured with st.cache_data on a 1,024-trial retained tree plus 300 reports: a filter click went from 2.6 s to 69 ms.
    • Uploads are keyed by file_id.
    • Download buttons use on_click="ignore", so a download no longer reruns the app.
    • A Reload reports button re-reads inputs from disk.
    • Adds upload-path tests, including the exact 16,000,000 / 16,000,001-byte boundary.
  • 7689b38: when inputs overlap (a directory plus a file or run inside it, or a latest symlink), each row is loaded once. Before, every pair turned into "ambiguous duplicate conditions". Byte-identical uploads are skipped with a notice.

Safety

  • 69c5bd7: report notices are shown as plain text.
    • st.warning renders Markdown, so an agent key in a shared report could embed a remote image or a link.
    • CSV formula escaping now also covers full-width =+-@ and whitespace-prefixed cells.
  • 90428c8: the launcher no longer runs code from, or reads settings in, the user's current directory.
    • It runs python -P from a dedicated dashboard_ui/ entry-script directory. Before, a streamlit.py or pandas.py in the current directory was executed, skillevaluator/reporting/html.py shadowed stdlib html inside the server, and a project .streamlit/config.toml could switch off CORS.
    • The Streamlit flags it now sets: showEmailPrompt=false (the old headless=false made a first run without a TTY exit 255 at the email prompt), fileWatcherType=none, toolbarMode=viewer, and allowed hosts limited to 127.0.0.1 and localhost.
  • 206c4c1: SIGTERM and SIGHUP are forwarded to Streamlit. Stopping the launcher from an IDE or with kill used to leave the server orphaned on the port.

UX

  • 689e4e5: new Comparison reason column, which says, for example, "without-skill arm status failed" or "different cases or attempts". The status filter now narrows only the performance table, like the condition filter. Before, filtering by status turned a failed pair into "missing condition".
  • 0842e3d: empty states now say when filters exclude everything, and a hint appears when no pair is comparable. A notice explains that no command records harness_features yet. Coverage text pluralizes "trial".
  • 2ca1739: per-trial token, time and cost columns, so rows with different trial counts can be compared.
  • 73bf1e6: without --port, Streamlit takes the next free port, so a second dashboard no longer fails on 8501. An explicit busy --port prints a hint.

Install and docs

  • 9fd8f57: skillevaluator is not on PyPI, so pip install "skillevaluator[dashboard]" fails, and the name could later be registered by someone else (dependency confusion). The error and docs now give the repo's uv tool install … "skillevaluator[all,dashboard] @ git+…" form, and dashboard is listed in the installation extras table (all does not include it).
  • caeffe0:
    • Dropped the two Unreleased Fixed bullets. Token omission shipped in v0.5.0 via feat(tier3): upgrade Harbor to 0.24.0 #83, and the dashboard itself is new.
    • Corrected the reports guide: pairing rule, time and cost sources, multi-step behaviour since v0.5.0, the agent_result token fallback, and the accepted harness_features forms and their precedence.

Hygiene

  • 96ba54e: ruff format on dashboard_app.py.
  • 3367504: tidy the retained-run loader with no behaviour change:
    • named bounds that mirror report_data's 512/4096 trial limits, pinned by a test;
    • a dead re-check and a double stat removed;
    • shared notice constants;
    • expected made a required keyword;
    • new tests for the trial-discovery limit and the 16 MiB local-file notice.
  • 878dd78: tests:
    • encoding="utf-8" on all 48 test file reads and writes, for the Windows job;
    • a 60 s AppTest ceiling with pandas and pyarrow imported first (a cold xdist run had two AppTest failures at 15 s);
    • dashboard is now pinned under Core workflows in --help.

Deferred, not in this PR

  • Adverse-vs-without comparisons: no producer emits adverse arms yet.
  • Producers writing cost and harness_features.
  • Aggregating across runs, and charts.
  • A single upload/file size limit: is 16 MB vs 16 MiB intentional?

A full inline review of the whole PR follows separately.

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review: 14 findings (0 P1, 5 P2, 9 P3); 14 posted inline.

Comment thread src/skillevaluator/reporting/dashboard_data.py
Comment thread src/skillevaluator/reporting/dashboard_data.py Outdated
Comment thread src/skillevaluator/reporting/dashboard_data.py Outdated
Comment thread src/skillevaluator/reporting/dashboard_app.py
Comment thread src/skillevaluator/reporting/dashboard_app.py Outdated
Comment thread docs/reports.mdx Outdated
Comment thread docs/reports.mdx Outdated
Comment thread tests/reporting/test_dashboard_data.py Outdated
Comment thread CHANGELOG.md Outdated
Comment thread tests/reporting/test_dashboard_data.py Outdated
rng1995 and others added 13 commits October 16, 2026 10:50
…ccurately

The reports guide said every loader truncation other than an oversized
trajectory blocks comparisons, but truncation limited to diagnostic details
leaves pairs comparable with a notice, as a test already pins. The CHANGELOG
entry placed per-trial means among the paired differences; they appear only
in the performance table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
…coverage

When a canonical condition listed fewer trials than num_trials, the usage
coverage denominator added the shortfall on top of the declared count, so
9 of 10 trials read "tokens 9/11". Trials missing from the declared count
are now counted once; totals still stay unavailable until every trial is
measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
…ned runs

Retained runs were parsed through the canonical builder with its 2 MiB
embedded-report budget, so a large multi-agent run lost every non-best
agent's trials and showed "0 trials" with no score. The builder now takes
enforce_embedded_size (default True for every other caller), and the
dashboard turns it off because it never embeds the payload; loader
truncation is still recorded.

Canonical JSON that was pruned this way still fails closed, but the row
now shows the declared trial count and the comparison reason says the
trial details were pruned and to load the retained run directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
…nreadable

A malformed, oversized, or symlinked trajectory.json made the whole
retained trial unmeasured, dropping the agent time and cost that
result.json records and the documented agent_result token fallback. The
canonical export keeps that time, so the retained run showed less than
its own export. Now only the trajectory is skipped, with a per-trial
notice, and tokens fall back to agent_result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
…g.json

The guide says to add dashboard_metadata to run_config.json by hand, but
the loader read that file only when result.json had no embedded
run_config, and every real run embeds one, so hand-added features stayed
"unknown". The run directory's run_config.json now wins; the embedded copy
is used, with a notice, only when the file cannot be read. The test
fixture now embeds run_config in result.json the way the runner does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
…vailable

Canonical exports written before v0.5.0 recorded missing token counters
as zero, and the dashboard showed those zeros as measured and published
token differences against them. When a report's evaluator_version is
earlier than 0.5.0, zero token counters are now unavailable and the
source gets a notice pointing to the retained run directory. Missing or
unparsable versions keep their values. Retained runs read counters from
disk and do not show the notice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
A directory input is cached on the directory's own fingerprint, which
does not change when a file inside it is rewritten in place. When the
directory came before that file on the command line, dedupe_rows kept
the directory's stale rows and dropped the file's fresh ones. Load the
deepest inputs first so the explicitly named file wins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
A directory input with a subdirectory the user cannot search (such as a
root-owned trial directory from a container run) raised PermissionError
from the native-run probe, and main() let it replace the whole page with
a traceback. The directory walk now records a notice for that
subdirectory and keeps going, and main() turns any unexpected load error
for one input into a notice so the other inputs still render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
The long-attempt test only set trial_id, which stopped carrying attempt
numbers when attempt pairing moved to the attempt and attempt_index
fields, so it passed regardless of how attempts were normalized. It now
sets a 5000-digit attempt with leading zeros on one arm and checks that
it pairs with the same value, and not with a different one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
The directory-walk test created a symlink without the OSError guard the
other symlink tests use, so it errored on Windows hosts without symlink
rights. The malformed and missing-input checks now live in their own
test, and the symlink test skips when the host cannot create one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
The launcher limits browser connections to 127.0.0.1 and localhost through
server.allowedHosts, which Streamlit added in 1.61. Older releases ignore
the setting without a warning, so the dashboard extra now requires 1.61.
A new test checks that every setting the launcher passes exists in the
installed Streamlit, and the packaging test pins the new floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
When a signal killed Streamlit, the launcher passed the negative return
code to SystemExit, so a forwarded SIGHUP exited 255 and SIGKILL 247.
The launcher now exits 128 plus the signal number (129 and 137), as a
shell does, and the busy-port hint stays limited to Streamlit's own errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
…ts it

The launcher assumed Ctrl+C always reached the whole process group. A
SIGINT sent to the launcher's pid alone left Streamlit serving the port
for 10 seconds until it was terminated. The launcher now sends SIGINT to
Streamlit before waiting, outside Windows. A new test covers the timeout
path that terminates a Streamlit that does not stop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
@rng1995

rng1995 commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

Review follow-up

I posted 14 verified inline findings (5 P2, 9 P3). All are fixed, replied to, and resolved, one commit each (the two docs findings share one). Head is now 60f62a9. The full local suite gives 11,874 passed; the 2 sidecar tests that fail on main in my environment still fail.

  • P2 9d2a16c: in large retained runs, non-best agents kept 0 trials because they went through the 2 MiB embedded-report pruning. agent_eval_result_from_directory now takes enforce_embedded_size, which defaults to on, and the dashboard turns it off for this read-only use. Pruned canonical JSON still fails closed, but it now shows the declared trial count and says why.
  • P2 f79c9d1: an unreadable subdirectory crashed the whole page. The directory walk now skips it with a notice, and main() turns any per-input error into a notice.
  • P2 da81f34: the allowed-hosts protection only exists from Streamlit 1.61, so the extra now requires streamlit>=1.61,<2. A test checks that every flag the launcher passes exists in the installed Streamlit.
  • P2 244477c: real runs embed run_config in result.json, so the hand-edited run_config.json that the docs describe was ignored. The file now takes precedence.
  • P2 ea19d4a: exports from before 0.5.0 that recorded unmeasured token counters as zero no longer produce token deltas. Those zeros are treated as unavailable, with a notice.
  • P3:
    • 6a2bc7c: the coverage denominator no longer counts missing trials twice.
    • 5ef4a47: an unreadable trajectory.json now drops only that trial's tokens; its time and cost are kept.
    • 09cea2b: when a file and its directory are both given as inputs and the file is rewritten, the new content now replaces the cached copy.
    • b8285c5: when Streamlit dies from a signal, the launcher exits with 128 plus the signal number.
    • 60f62a9: SIGINT sent only to the launcher is now passed on to Streamlit.
    • 0ae439c, 4badacf: two tests were tightened.
    • b67c297: two docs and CHANGELOG corrections.

@chrisknvidia, three decisions are yours:

  • Please confirm the repeated-attempt pairing change (a6084f0).
  • Please confirm the new enforce_embedded_size keyword on the Tier 3 builder.
  • Is the 16 MB upload limit vs the 16 MiB local-file limit intentional?

rng1995 and others added 4 commits October 16, 2026 11:19
Resolve the THIRD_PARTY_NOTICES.md conflict with #77 by keeping the
dashboard row in the dependency table and main's new Unicode security data
section. `uv lock --check` passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
…ompacted

Since #77 the embedded-size budget compacts a non-best agent's conditions
(status, errors and attempt counts) instead of dropping them, so requiring
empty conditions no longer recognized a pruned agent. Its canonical row then
read "0 with / 0 without" with a generic reason. Detect pruning from the
truncation marker and the emptied trial lists only, which covers both report
shapes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
The launcher tests replaced importlib.util.find_spec for every module with a
stub that returned a bare object(). Under coverage, a sys.path change makes
coverage look up the measured package through find_spec while the CLI runs;
it got the stub, raised AttributeError ('object' has no attribute 'origin'),
and the command exited 1. The Python 3.12 CI job (serial, with coverage)
failed test_launch_uses_loopback_and_literal_absolute_paths and
test_explicit_port_failure_has_hint this way. Fake only the "streamlit"
lookup, with a real ModuleSpec, and pass every other name through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Merge #124 cleanly. `uv lock --check` passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.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.

[FEA]: create a tables and rows quantitative dashboard to compare agent skill performance

2 participants