Skip to content

feat(analysis): turn-messages viewer with vendored fzstd decompressor#1115

Open
ajcasagrande wants to merge 1 commit into
mainfrom
ajc/turn-messages-viewer
Open

feat(analysis): turn-messages viewer with vendored fzstd decompressor#1115
ajcasagrande wants to merge 1 commit into
mainfrom
ajc/turn-messages-viewer

Conversation

@ajcasagrande

@ajcasagrande ajcasagrande commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Carve-out from the agentx branch (ajc/agentx-on-main): a self-contained HTML viewer of the per-turn INPUT messages a run actually sent, plus the vendored pure-JS zstd decompressor it inflates its embedded payload with client-side.

Stacked on #1102 (ajc/metrics-accumulator-engine). Do not merge before it.

aiperf turn-messages <run_dir> reads --export-level raw output (raw_records/*.jsonl shards or profile_export_raw.jsonl) and renders a lazy conversation → turn → message dropdown tree. The payload is interned (each unique (role, content) stored once), zstd-compressed with a 64 MB long-distance window (pinned to fzstd-safe windowLog=26 and round-trip self-checked before writing), base64-embedded, and inflated in the browser by the inlined fzstd.umd.js — no network, no WASM, no DecompressionStream.

Files

  • src/aiperf/analysis/turn_messages.py — raw-record loader, message interning, payload builder, HTML writer
  • src/aiperf/analysis/turn_messages_viewer.html — viewer template (lazy DOM, filter, roots-only toggle)
  • src/aiperf/analysis/fzstd.umd.js — vendored fzstd v0.1.1 (10,029 bytes; MIT)
  • src/aiperf/cli_commands/turn_messages.pyaiperf turn-messages CLI command
  • src/aiperf/cli.py — one wiring line (turn-messages only)
  • tests/unit/analysis/test_turn_messages.py — 33 tests: interning, zstd round-trip within the fzstd-safe window, adversarial/malformed-record tolerance, HTML-injection containment, CLI exit codes
  • ATTRIBUTIONS.md — fzstd entry only
  • pyproject.tomlclen added to codespell ignore-words-list (identifier in turn_messages.py)
  • tools/ruff_baseline.json / tools/ergonomics_baseline.json — baseline entries for build_payload (C901 11>10, keyword-only-args) and the CLI entrypoint, same entries the agentx branch carries
  • docs/cli-options.md — regenerated via make generate-cli-docs

Deliberate overlap with ajc/swim-lane-viewer

The sibling swim-lane PR owns src/aiperf/cli_commands/analyze.py and the analyze group wiring in cli.py. Both PRs add one adjacent line to the command-registration block in cli.py, so whichever merges second gets a trivial rebase there. Post-merge follow-up: once the swim-lane PR's analyze.py lands, add the aiperf.cli_commands.turn_messages:app hook to the analyze group (nesting this command as aiperf analyze turn-messages) instead of duplicating analyze.py here.

fzstd attribution

fzstd.umd.js is the vendored UMD build of 101arrowz/fzstd v0.1.1 (MIT, Copyright (c) 2020 Arjun Barrett). The full license text is carried in the file header comment and in ATTRIBUTIONS.md under "Code Assets". Hygiene: 10,029 bytes (clears the 5000 KB check-added-large-files threshold), the add-license hook has no .js handler so it is skipped, and the file passes codespell clean with no skip entry needed.

Verification

  • make first-time-setup clean
  • import smoke: aiperf.analysis.turn_messages + aiperf.cli_commands.turn_messages import, template/decoder assets resolve
  • uv run pytest --collect-only -q tests/unit/ — 14309/14396 collected (87 deselected), no collection errors
  • uv run pytest -n auto tests/unit/analysis/171 passed
  • ruff format --check + ruff check clean on all changed .py files
  • make check-ruff-baselined — OK (120 total, 120 baselined, 0 new)
  • make check-ergonomics — OK (63 total, 63 baselined, 0 new)
  • Smoke: uv run aiperf turn-messages --help clean; a 5-turn accumulated-history fixture rendered a 19 KB turn_messages.html; the embedded payload base64+zstd round-trips under a windowLog=26-capped decoder (1 conversations (1 shown) · 5 turns · 30 message refs (10 unique)), fzstd is inlined (.fzstd=f() / fzstd.decompress(), and no template placeholders remain

Gotcha sweep (#1096 postmortem): every field turn_messages.py reads from raw exports (conversation_id, turn_index, agent_depth, parent_correlation_id, x_correlation_id, benchmark_phase, was_cancelled, request_start_ns) verified present on MetricRecordMetadata in this base tree; OutputDefaults.RAW_RECORDS_FOLDER / PROFILE_EXPORT_RAW_JSONL_FILE verified in src/aiperf/config/artifacts.py; no imports from other aiperf.analysis modules, so no coupling to the sweepline code from #1102.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a new turn-messages command that generates a self-contained HTML viewer for per-turn input messages from one or more run directories.
    • The viewer supports conversation filtering, expandable turn trees, and limits for conversations, turns, and message length.
  • Documentation

    • Documented the new command and its CLI options in the command reference.
  • Chores

    • Updated attribution and spelling-check settings to account for newly included content.

@ajcasagrande ajcasagrande added the AgentX Feature for AgentX label Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Try out this PR

Quick install:

pip install --upgrade --force-reinstall git+https://github.com/ai-dynamo/aiperf.git@0651c380d600094d3da15b3378dd3911458e925d

Recommended with virtual environment (using uv):

uv venv --python 3.12 && source .venv/bin/activate
uv pip install --upgrade --force-reinstall git+https://github.com/ai-dynamo/aiperf.git@0651c380d600094d3da15b3378dd3911458e925d

Last updated for commit: 0651c38Browse code

@github-actions github-actions Bot added the feat label Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.20670% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/aiperf/analysis/turn_messages.py 96.79% 2 Missing and 3 partials ⚠️

📢 Thoughts on this report? Let us know!

Base automatically changed from ajc/metrics-accumulator-engine to main July 8, 2026 18:42
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
@ajcasagrande ajcasagrande force-pushed the ajc/turn-messages-viewer branch from 30ce0e7 to 0651c38 Compare July 8, 2026 20:35
@ajcasagrande ajcasagrande marked this pull request as ready for review July 8, 2026 20:45
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds an aiperf turn-messages CLI command that generates a self-contained, zstd-compressed HTML viewer for per-turn input messages from AIPerf run directories. It includes the payload builder, an embedded fzstd decoder with MIT attribution, CLI wiring, documentation, and comprehensive tests.

Changes

Turn Messages Viewer Feature

Layer / File(s) Summary
Payload building and HTML generation core
src/aiperf/analysis/turn_messages.py
Loads and flattens raw JSONL records, builds a compact payload with interned messages and truncation tracking, and writes zstd-compressed, self-verifying HTML output.
Client-side HTML viewer
src/aiperf/analysis/turn_messages_viewer.html
Standalone dark-themed HTML page that inflates the embedded compressed payload via inlined fzstd and lazily renders conversations, turns, and messages with filtering.
CLI command and app registration
src/aiperf/cli_commands/turn_messages.py, src/aiperf/cli.py
Adds the turn-messages cyclopts command with validation, per-run-dir generation and reporting, and registers it in the main CLI.
Unit tests for payload, viewer, and CLI
tests/unit/analysis/test_turn_messages.py
Tests payload interning/truncation, HTML round-trip decoding, injection resistance, edge cases, and CLI exit-code behavior.
Docs, attributions, and baseline updates
docs/cli-options.md, ATTRIBUTIONS.md, src/aiperf/analysis/fzstd.umd.js, pyproject.toml, tools/ergonomics_baseline.json, tools/ruff_baseline.json
Documents the new command, attributes vendored fzstd under MIT, notes a windowLog corruption caveat, and updates codespell ignore list and lint baselines.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Poem

A rabbit hopped through logs so raw,
Compressed each turn without a flaw,
With fzstd tucked inside so neat,
No server needed—just a treat! 🐇
Click, expand, and watch messages bloom,
All snug within one HTML room.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: a new turn-messages viewer that uses a vendored fzstd decompressor.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

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

@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: 2

🧹 Nitpick comments (1)
tests/unit/analysis/test_turn_messages.py (1)

59-67: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: prefer unpacking over list concatenation.

Static analysis (RUF005) flags both history + [...] concatenations; unpacking is more idiomatic and avoids an extra list creation.

♻️ Proposed fix
     for t in range(n_turns):
-        history = history + [{"role": "user", "content": f"question {t}"}]
+        history = [*history, {"role": "user", "content": f"question {t}"}]
         recs.append(_rec(conv, t, list(history), start_s=float(t)))
-        history = history + [{"role": "assistant", "content": f"answer {t}"}]
+        history = [*history, {"role": "assistant", "content": f"answer {t}"}]
🤖 Prompt for 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.

In `@tests/unit/analysis/test_turn_messages.py` around lines 59 - 67, The
`_accumulated` helper in `test_turn_messages.py` uses `history + [...]` twice,
which triggers RUF005 and creates unnecessary intermediate lists. Update the
`history` updates to use unpacking in the same `_accumulated` function so the
list is extended idiomatically without concatenation. Keep the existing behavior
and structure of `_rec` calls unchanged.

Source: Linters/SAST tools

🤖 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 `@docs/cli-options.md`:
- Around line 3033-3060: The new aiperf turn-messages docs section skips heading
levels and jumps from the H2 title to H4 option entries, causing the markdown
hierarchy to be invalid. Update the section structure so the option list is
nested under an intermediate H3 (or otherwise regenerate the headings) while
preserving the existing content around aiperf turn-messages, --run-dirs, --out,
--limit-conversations, --max-turns, and --content-cap.

In `@src/aiperf/analysis/turn_messages.py`:
- Around line 358-368: The HTML generation in turn_messages currently relies on
default text encoding for VIEWER_TEMPLATE.read_text(), FZSTD_JS.read_text(), and
out_path.write_text(), which can fail on Windows with non-ASCII content. Update
the template reads and the final write in turn_messages to explicitly use utf-8
encoding, keeping the existing error handling and avoiding
safe_read_template_path since VIEWER_TEMPLATE and FZSTD_JS are internal Path
objects, not user-supplied paths.

---

Nitpick comments:
In `@tests/unit/analysis/test_turn_messages.py`:
- Around line 59-67: The `_accumulated` helper in `test_turn_messages.py` uses
`history + [...]` twice, which triggers RUF005 and creates unnecessary
intermediate lists. Update the `history` updates to use unpacking in the same
`_accumulated` function so the list is extended idiomatically without
concatenation. Keep the existing behavior and structure of `_rec` calls
unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f86b47eb-ab72-4d4b-8d77-a20e7b4a50a2

📥 Commits

Reviewing files that changed from the base of the PR and between d41bd50 and 0651c38.

📒 Files selected for processing (11)
  • ATTRIBUTIONS.md
  • docs/cli-options.md
  • pyproject.toml
  • src/aiperf/analysis/fzstd.umd.js
  • src/aiperf/analysis/turn_messages.py
  • src/aiperf/analysis/turn_messages_viewer.html
  • src/aiperf/cli.py
  • src/aiperf/cli_commands/turn_messages.py
  • tests/unit/analysis/test_turn_messages.py
  • tools/ergonomics_baseline.json
  • tools/ruff_baseline.json

Comment thread docs/cli-options.md
Comment on lines +3033 to +3060
## `aiperf turn-messages`

Render a collapsible HTML viewer of the per-turn input messages a run sent.

#### `--run-dirs`, `--empty-run-dirs` `<list>` _(Required)_

One or more AIPerf run directories.

#### `-o`, `--out` `<str>`

Output HTML path. Only valid when a single run directory is given.

#### `-n`, `--limit-conversations` `<int>`

Max conversations to render (roots first, then by earliest request time).
<br/>_Default: `40`_

#### `--max-turns` `<int>`

Max turns rendered per conversation; the rest are summarized as a hidden count.
<br/>_Default: `60`_

#### `--content-cap` `<int>`

Max characters kept per unique message body; longer bodies are truncated with a remaining-chars note. Raise for full fidelity.
<br/>_Default: `8000`_

<hr/>

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the heading hierarchy valid. The new section jumps from H2 to H4, which triggers MD001 here. Add an intermediate H3 (or regenerate the section structure) so the docs stay lint-clean.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 3037-3037: Heading levels should only increment by one level at a time
Expected: h3; Actual: h4

(MD001, heading-increment)

🤖 Prompt for 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.

In `@docs/cli-options.md` around lines 3033 - 3060, The new aiperf turn-messages
docs section skips heading levels and jumps from the H2 title to H4 option
entries, causing the markdown hierarchy to be invalid. Update the section
structure so the option list is nested under an intermediate H3 (or otherwise
regenerate the headings) while preserving the existing content around aiperf
turn-messages, --run-dirs, --out, --limit-conversations, --max-turns, and
--content-cap.

Source: Linters/SAST tools

Comment on lines +358 to +368
html = (
VIEWER_TEMPLATE.read_text()
.replace("__FZSTD_JS__", FZSTD_JS.read_text())
.replace("__PAYLOAD_B64__", payload_b64)
)
default_dir = run_dir.parent if run_dir.is_file() else run_dir
out_path = out or (default_dir / "turn_messages.html")
try:
out_path.write_text(html)
except OSError as e:
raise TurnMessagesError(f"could not write {out_path}: {e}") from e

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm helper location/signature so the read replacement is correct.
fd -t f path_safety.py src | xargs -r -I{} sh -c 'echo "== {} =="; cat -n {}'
rg -nP 'def\s+safe_read_template_path' -C2

Repository: ai-dynamo/aiperf

Length of output: 2807


🏁 Script executed:

#!/bin/bash
# Find and read the relevant section of turn_messages.py
fd -t f turn_messages.py src | head -1 | xargs -r -I{} sh -c '
  echo "=== Full file overview ==="
  wc -l {}
  echo ""
  echo "=== Imports and constants at top ==="
  head -50 {}
  echo ""
  echo "=== Lines around 358 (target area) ==="
  sed -n "340,380p" {}
'

Repository: ai-dynamo/aiperf

Length of output: 3881


🏁 Script executed:

#!/bin/bash
# Check imports and see correct pattern
fd -t f turn_messages.py src | head -1 | xargs -r -I{} sh -c '
  echo "=== All imports in turn_messages.py ==="
  grep -n "^from\|^import" {}
  echo ""
  echo "=== Any existing use of safe_read_template_path in codebase ==="
  rg -n "safe_read_template_path" --type py -C2
'

Repository: ai-dynamo/aiperf

Length of output: 7388


🏁 Script executed:

#!/bin/bash
# Check if VIEWER_TEMPLATE and FZSTD_JS are internal package data or user-supplied
rg -B5 -A2 "VIEWER_TEMPLATE\s*=" src/aiperf/analysis/turn_messages.py
echo "---"
# Check the docstring/context of safe_read_template_path more carefully
sed -n "4,12p" src/aiperf/common/path_safety.py

Repository: ai-dynamo/aiperf

Length of output: 845


Use encoding="utf-8" for template reads and HTML writes to prevent Windows crashes.

read_text() and write_text() default to platform encoding (cp1252 on Windows), which will raise UnicodeDecodeError/UnicodeEncodeError on the non-ASCII characters in the viewer template and payload (▸ … · × —). The fix is straightforward: add encoding="utf-8" to all three calls.

Note: VIEWER_TEMPLATE and FZSTD_JS are internal package-data Path objects, not user-supplied strings, so they should not use safe_read_template_path (which is designed for CWE-22 hardening of user-supplied template paths and requires str input plus None-handling).

Proposed fix
     payload_b64 = base64.b64encode(compressed).decode()
     html = (
-        VIEWER_TEMPLATE.read_text()
-        .replace("__FZSTD_JS__", FZSTD_JS.read_text())
+        VIEWER_TEMPLATE.read_text(encoding="utf-8")
+        .replace("__FZSTD_JS__", FZSTD_JS.read_text(encoding="utf-8"))
         .replace("__PAYLOAD_B64__", payload_b64)
     )
     default_dir = run_dir.parent if run_dir.is_file() else run_dir
     out_path = out or (default_dir / "turn_messages.html")
     try:
-        out_path.write_text(html)
+        out_path.write_text(html, encoding="utf-8")
     except OSError as e:
         raise TurnMessagesError(f"could not write {out_path}: {e}") from e
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
html = (
VIEWER_TEMPLATE.read_text()
.replace("__FZSTD_JS__", FZSTD_JS.read_text())
.replace("__PAYLOAD_B64__", payload_b64)
)
default_dir = run_dir.parent if run_dir.is_file() else run_dir
out_path = out or (default_dir / "turn_messages.html")
try:
out_path.write_text(html)
except OSError as e:
raise TurnMessagesError(f"could not write {out_path}: {e}") from e
html = (
VIEWER_TEMPLATE.read_text(encoding="utf-8")
.replace("__FZSTD_JS__", FZSTD_JS.read_text(encoding="utf-8"))
.replace("__PAYLOAD_B64__", payload_b64)
)
default_dir = run_dir.parent if run_dir.is_file() else run_dir
out_path = out or (default_dir / "turn_messages.html")
try:
out_path.write_text(html, encoding="utf-8")
except OSError as e:
raise TurnMessagesError(f"could not write {out_path}: {e}") from e
🤖 Prompt for 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.

In `@src/aiperf/analysis/turn_messages.py` around lines 358 - 368, The HTML
generation in turn_messages currently relies on default text encoding for
VIEWER_TEMPLATE.read_text(), FZSTD_JS.read_text(), and out_path.write_text(),
which can fail on Windows with non-ASCII content. Update the template reads and
the final write in turn_messages to explicitly use utf-8 encoding, keeping the
existing error handling and avoiding safe_read_template_path since
VIEWER_TEMPLATE and FZSTD_JS are internal Path objects, not user-supplied paths.

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AgentX Feature for AgentX feat

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant