feat(desktop): add Agent Usage UI backed by the NIP-AM local archive - #2035
Open
wpfleger96 wants to merge 1 commit into
Open
feat(desktop): add Agent Usage UI backed by the NIP-AM local archive#2035wpfleger96 wants to merge 1 commit into
wpfleger96 wants to merge 1 commit into
Conversation
wpfleger96
marked this pull request as draft
July 17, 2026 17:10
wpfleger96
force-pushed
the
duncan/agent-usage-archive
branch
4 times, most recently
from
July 21, 2026 08:04
b213a00 to
122b043
Compare
wpfleger96
pushed a commit
that referenced
this pull request
Jul 21, 2026
wpfleger96
pushed a commit
that referenced
this pull request
Jul 23, 2026
wpfleger96
added a commit
that referenced
this pull request
Jul 23, 2026
Four Playwright tests covering the new Agent Usage UI surfaces: 01 overview card (default/collapsed), 02 focused usage subview, 03 multi-day bars chart, 04 empty state (collection on, no data). Also adds agent-usage-screenshots.spec.ts to the smoke project testMatch so CI can discover and run it alongside the other screenshot specs. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
added a commit
that referenced
this pull request
Jul 25, 2026
Stack: #2035 → this PR ## What Group the per-agent model breakdown by `(harness, model)` instead of `model` alone, so the same model running under two different harnesses (e.g. `claude-sonnet` via `goose` vs `claude-code`) produces two distinct rows rather than collapsing into one. ## Changes **Rust / SQLite** - `agent_metric_index`: add nullable `harness TEXT` column. Schema migration M1: `ALTER TABLE ... ADD COLUMN` + delete-then-backfill rebuild so all existing rows get `harness` populated from retained `archived_events.raw_json` — no data loss; ingest and backfill share the same `from_payload` parser (frozen-plan requirement preserved). - `AgentMetricIndexRow`: parse `harness` from `AgentTurnMetricPayload.harness` (REQUIRED field per NIP-AM), write/read through all store paths (`ROW_COLUMNS`, INSERT, `row_from_sql`). - `AgentScope.models` grouping key widened from `Option<String>` to `(Option<String>, Option<String>)` i.e. `(harness, model)`. Sort tiebreak: harness ascending → model ascending, `None` last in each. - `ModelUsage` wire type: add `harness: Option<String>` field. **Frontend** - `tauriArchive.ts` / `bridge.ts`: add `harness` to `AgentUsageModel` type. - `AgentUsageFocusedView`: render `harness` as a dimmed sub-label next to each model name on breakdown rows. `null` harness (pre-migration data or unknown) renders no label — single-harness data is visually unchanged. - `agentUsage.ts` `sortModelsByKnownTotal`: tiebreak updated for compound `(harness, model)` key. **Tests** - Rust: collapse-fix test (same model / two harnesses → two rows), migration test (old-shape rows get harness populated after rebuild), harness sort coverage. - TS: `sortModelsByKnownTotal` harness tiebreak tests, same-model two-harness sort test. - E2E: `bridge.ts` fixture type updated, `agent-usage.spec.ts` harness assertion, `agent-usage-screenshots.spec.ts` shot 02 harness-label visibility check. ## Gates All green locally: - `just desktop-check` ✓ - `just desktop-typecheck` ✓ - `just desktop-build` ✓ - `just desktop-test` (3487 pass, 0 fail) ✓ - `cargo test` in `desktop/src-tauri` (1575 pass, 0 fail) ✓ --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
wpfleger96
pushed a commit
that referenced
this pull request
Jul 25, 2026
wpfleger96
force-pushed
the
duncan/agent-usage-archive
branch
from
July 25, 2026 20:43
0d52530 to
0a5b070
Compare
wpfleger96
added a commit
that referenced
this pull request
Jul 29, 2026
…ain (#3593) ## What Wires genuine provider-reported `total_tokens` through the full buzz-agent → buzz-acp publish chain so kind-44200 events carry real per-turn and cumulative totals for OpenAI-backed models, while preserving all existing behaviour for Anthropic and external harnesses (goose, claude-code). ## Why Live prod data showed 0 of 1,934 archived reports carry `totalTokens`. Both hardcoded `total_tokens: None` in `pool.rs` and the absent field in `buzz-agent`'s parser are root causes. This is the backend half of a two-track fix; the display-fallback half lands in [#2035](#2035). ## Changes **`crates/buzz-agent/src/types.rs`** - Added `total_tokens: Option<u64>` to `LlmResponse` with an explicit doc comment that NIP-AM forbids deriving it. - Added `TurnTotalState` enum (`Unseen | Exact(u64) | Unknown`) with `fold()` and `exact_value()` — the tri-state accumulator that distinguishes not-yet-observed from permanently poisoned. **`crates/buzz-agent/src/llm.rs`** - `parse_responses` and `parse_openai`: read `usage.total_tokens` from OpenAI Chat Completions (including Databricks routes) and the Responses API via `sum_usage`. - Anthropic: explicit `total_tokens: None` — no genuine total available; NIP-AM forbids summing categories. **`crates/buzz-agent/src/agent.rs`** - Added `turn_total_state: &'a mut TurnTotalState` to `RunCtx`. - Fold `response.total_tokens` into the accumulator after each usage-bearing response; non-usage-bearing responses (keepalive/stream frames) do not poison. **`crates/buzz-agent/src/lib.rs`** - Added `accumulated_total_state: TurnTotalState` to `Session` (default `Unseen`). - Per-turn state passed to `RunCtx`, folded into session cumulative after each turn. - Emits `accumulatedTotalTokens` in `usage_update` only when cumulative is `Exact(n)`. **`crates/buzz-acp/src/usage.rs`** - Added `accumulated_total_tokens: Option<u64>` (serde default) to `UsageUpdatePayload` — optional for goose compat. - Added `last_total: Option<u64>` to `SessionState`. - Added `turn_total_tokens` and `cumulative_total_tokens` to `TurnUsage` (field-local — never affect `delta_reliable`). - Derive turn-total delta only when prev and current are both `Some` and monotonic; absence, decrease, or no baseline leaves only the total delta null without touching input/output reliability. **`crates/buzz-acp/src/pool.rs`** - Replaced both hardcoded `total_tokens: None` in `publish_agent_turn_metric` with `usage.turn_total_tokens` and `usage.cumulative_total_tokens`. ## Tests 20 new tests across the four touched files: | File | Tests | |------|-------| | `types.rs` | `TurnTotalState` fold, accumulation, exact_value, default (7 tests) | | `llm.rs` | Chat present/absent, Responses present/absent, Anthropic always-None (5 tests) | | `usage.rs` | First turn no baseline, second-turn delta, cumulative decrease (field-local), current absent, goose-shaped deserialization, baseline absent (6 tests) | | `pool.rs` | Exact turn+cumulative mapping, null totals never derived (2 tests) | `cargo test -p buzz-acp -p buzz-agent` — all passing, 0 failures. ## Scope Boundary: `crates/buzz-agent/**` + `crates/buzz-acp/**` only. Desktop unchanged. `costUsd` explicitly out of scope. Related: [#2035](#2035) --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
wpfleger96
pushed a commit
that referenced
this pull request
Jul 30, 2026
Member
Author
wpfleger96
marked this pull request as ready for review
July 30, 2026 21:02
wpfleger96
force-pushed
the
duncan/agent-usage-archive
branch
from
July 31, 2026 15:43
7855777 to
41b77f3
Compare
This was referenced Jul 31, 2026
wpfleger96
added a commit
that referenced
this pull request
Jul 31, 2026
Add the NIP-AM agent-usage archive backend (query engine, SQLite migrations, ingestion pipeline, buzz-acp first-turn fix) from PR #2035. This is the backend-only base of a stacked split; the UI lives in the companion stack PR. Backend surface: - `desktop/src-tauri/src/archive/`: archive query engine, metric index store, store migrations, pipeline integration, and full Rust test suite (agent_usage, metric_store, store, migration tests) - `desktop/src/shared/api/tauriArchive.ts`: wire types + Tauri command bridge (`get_agent_usage_series`) - `desktop/src/features/local-archive/archiveSyncManager.ts`: `persistedAgentMetrics` notifier wired to the archiveSyncManager - `crates/buzz-acp/`: first-turn baseline fix (Task E from #2035) Cache-read schema addition (M2 migration): Adds `turn_cache_read_tokens` and `cumulative_cache_read_tokens` columns to `agent_metric_index` via a new one-shot additive migration (M2). These columns persist the optional NIP-AM `cacheReadTokens` fields so that Hayt's Phase-0 cache-threading events (crates/buzz-acp, separate PR) start being persisted the day both PRs merge. All pre-migration rows receive NULL (fail-closed). M2 runs before M1 in the migration chain so the M1 rebuild always operates against the full schema. Three M2 migration tests: fresh DB, post-M1 upgrade, and idempotency. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Adds a rolling token/cost usage view for agents, sourced from the local NIP-AM metrics archive. Covers the Agents overview and a focused per-agent subview in the profile panel, with the per-agent breakdown grouped by (harness, model) so the same model under two harnesses renders as two distinct rows. Backend: - get_agent_usage_series Tauri command (archive/agent_usage.rs): reads the local SQLite archive over a sliding 7/30-day window, aggregates by (agent_pubkey, session_id, harness, model), and returns a serialized AgentUsageSeries response. - archive/metric_store.rs: MetricIndexRow + insert_metric_index_row with turn/cumulative token + cost + cache-read fields; query layer produces AgentMetricRow slices. - store_migrations.rs: M1 (harness column + index rebuild) and M2 (cache-read columns: turn_cache_read_tokens, cumulative_cache_read_tokens). M2 runs before M1 so the rebuild always operates against the full schema. All migrations are idempotent via marker guard. - archive/mod.rs: pipeline integration — AgentMetricRow ingestion, persistedAgentMetrics notifier, archiveSyncManager wiring. - tauriArchive.ts: wire types + Tauri command bridge. - archiveSyncManager.ts: persistedAgentMetrics notifier. Agent-side first-turn accounting (buzz-acp): - UsageTracker::seed_zero_baseline() seeds a zero baseline for sessions this process just spawned, so the first turn of a fresh session reports reliable deltas. Re-attached sessions are not seeded; their true prior cumulative is unknown. Seeding is idempotent via entry().or_insert(). acp.rs call sites are immediately after create_session_and_apply_model() on both channel and heartbeat paths. UI: - desktop/src/features/agent-usage/: AgentUsageDailyBars (recharts), AgentUsageRangeTabs (7/30-day preset + custom), AgentUsageFocusedView (per-agent breakdown by harness/model), AgentUsageSection (wires overview + focused view). hooks.ts: data fetch and memoised series. lib/agentUsage.ts: bucket math, formatter, caveat detection, DisplayTotal, sortModelsByKnownTotal. - Profile panel: canViewUsage/onOpenUsage threaded; BarChart3 ingress row in the Info tab. AgentsView/AgentsScreen reordered. - ProfilePanelContext: usage navigation state. - e2eBridge.ts + tests/helpers/bridge.ts: get_agent_usage_series mock handler + fixture types. - agent-usage.spec.ts: 19-test Playwright spec (loading, empty, bars, range switch, error/retry, focused view, caveats, cache-invalidation). - agent-usage-screenshots.spec.ts: 3-test screenshot spec. - playwright.config.ts: agent-usage specs added to suite. - package.json: recharts dependency. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
force-pushed
the
duncan/agent-usage-archive
branch
from
August 1, 2026 00:23
3d6c9c4 to
4d18b86
Compare
calvadev
pushed a commit
to shopstr-eng/buzz
that referenced
this pull request
Aug 3, 2026
…ain (block#3593) ## What Wires genuine provider-reported `total_tokens` through the full buzz-agent → buzz-acp publish chain so kind-44200 events carry real per-turn and cumulative totals for OpenAI-backed models, while preserving all existing behaviour for Anthropic and external harnesses (goose, claude-code). ## Why Live prod data showed 0 of 1,934 archived reports carry `totalTokens`. Both hardcoded `total_tokens: None` in `pool.rs` and the absent field in `buzz-agent`'s parser are root causes. This is the backend half of a two-track fix; the display-fallback half lands in [block#2035](block#2035). ## Changes **`crates/buzz-agent/src/types.rs`** - Added `total_tokens: Option<u64>` to `LlmResponse` with an explicit doc comment that NIP-AM forbids deriving it. - Added `TurnTotalState` enum (`Unseen | Exact(u64) | Unknown`) with `fold()` and `exact_value()` — the tri-state accumulator that distinguishes not-yet-observed from permanently poisoned. **`crates/buzz-agent/src/llm.rs`** - `parse_responses` and `parse_openai`: read `usage.total_tokens` from OpenAI Chat Completions (including Databricks routes) and the Responses API via `sum_usage`. - Anthropic: explicit `total_tokens: None` — no genuine total available; NIP-AM forbids summing categories. **`crates/buzz-agent/src/agent.rs`** - Added `turn_total_state: &'a mut TurnTotalState` to `RunCtx`. - Fold `response.total_tokens` into the accumulator after each usage-bearing response; non-usage-bearing responses (keepalive/stream frames) do not poison. **`crates/buzz-agent/src/lib.rs`** - Added `accumulated_total_state: TurnTotalState` to `Session` (default `Unseen`). - Per-turn state passed to `RunCtx`, folded into session cumulative after each turn. - Emits `accumulatedTotalTokens` in `usage_update` only when cumulative is `Exact(n)`. **`crates/buzz-acp/src/usage.rs`** - Added `accumulated_total_tokens: Option<u64>` (serde default) to `UsageUpdatePayload` — optional for goose compat. - Added `last_total: Option<u64>` to `SessionState`. - Added `turn_total_tokens` and `cumulative_total_tokens` to `TurnUsage` (field-local — never affect `delta_reliable`). - Derive turn-total delta only when prev and current are both `Some` and monotonic; absence, decrease, or no baseline leaves only the total delta null without touching input/output reliability. **`crates/buzz-acp/src/pool.rs`** - Replaced both hardcoded `total_tokens: None` in `publish_agent_turn_metric` with `usage.turn_total_tokens` and `usage.cumulative_total_tokens`. ## Tests 20 new tests across the four touched files: | File | Tests | |------|-------| | `types.rs` | `TurnTotalState` fold, accumulation, exact_value, default (7 tests) | | `llm.rs` | Chat present/absent, Responses present/absent, Anthropic always-None (5 tests) | | `usage.rs` | First turn no baseline, second-turn delta, cumulative decrease (field-local), current absent, goose-shaped deserialization, baseline absent (6 tests) | | `pool.rs` | Exact turn+cumulative mapping, null totals never derived (2 tests) | `cargo test -p buzz-acp -p buzz-agent` — all passing, 0 failures. ## Scope Boundary: `crates/buzz-agent/**` + `crates/buzz-acp/**` only. Desktop unchanged. `costUsd` explicitly out of scope. Related: [block#2035](block#2035) --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.







Adds a rolling token/cost usage view for agents, sourced from the local NIP-AM metrics archive. Covers both the Agents overview and a focused per-agent subview in the profile panel, with the per-agent breakdown grouped by
(harness, model)so the same model running under two different harnesses (e.g.claude-sonnet-4-5viagoosevsclaude-code) renders as two distinct rows.Backend
get_agent_usage_seriesTauri command (archive/agent_usage.rs): reads the local SQLite archive over a caller-supplied window of local-midnight bucket boundaries, returns per-agent token and cost totals broken down by(harness, model), with partial/unknown-field flags when evidence is incomplete. Sort tiebreak: harness ascending → model ascending,Nonelast in each.MIN_BOUNDARIES = 2(the single-bucket1dcase) throughMAX_BOUNDARIES = 367(366 daily buckets, one leap year). The frontend picker clamps to the same span, so this fail-closed check is defense in depth and never the UX error path.MAX_INTERVAL_SECSstill bounds each individual bucket at 48h.agent_metric_index: nullableharness TEXTcolumn, parsed fromAgentTurnMetricPayload.harnessand written/read through all store paths. Schema migration M1 runs as a single SQLite transaction — schema-guardedALTER TABLE, full index rebuild from the canonicalarchived_eventsstore (ingest and backfill share the samefrom_payloadparser), completion marker written last — so a crash mid-migration can never leave a half-built index marked complete.persistedAgentMetricsnotifier through the archive sync path so new metric events are picked up without a full resync.Frontend
AgentUsageSection(features/agent-usage/ui/): ranked agent list inAgentsView, rendered below the agent cards and the teams section. Row click-through opens the profile panel focused on usage.AgentUsageDailyBars: each bar carries its date on the x-axis and its compact token total above it, plus a per-bar hover tooltip with the exact total/input/output split. Each tooltip field reports "unknown" independently rather than collapsing an uncounted value to zero. Above 14 buckets the chart drops on-bar value text and thins date ticks instead of overflowing; the tooltip stays complete.AgentUsageRangeTabs:1d/7d/30dpresets plus aCustomtab whose popover takes an arbitrary inclusive start/end date pair. SelectingCustomopens the picker without moving the active range, so an in-progress edit never issues a query. Validation is local — the user sees "Pick a range of 366 days or fewer", not a backend arity error.AgentUsageFocusedView: per-agent totals with a by-(harness, model)breakdown, rendered as a focused profile-panel subview (same pattern as Memories/Diagnostics) rather than a new tab. Each breakdown row shows the harness as a dimmed sub-label next to the model name;nullharness (pre-migration or unknown data) renders no label, so single-harness data is visually unchanged.UserProfilePanel/UserProfilePanelSections/UserProfilePanelTabs: threadedcanViewUsage/onOpenUsage(owner-only, bot profiles only) and added aBarChart3ingress row in the Info tab.agentUsage.tssortModelsByKnownTotal: ordinal compound(harness, model)tiebreak matching the Rust ordering (identifier domain is ASCII; documented).Agent-side first-turn accounting
UsageTracker::seed_zero_baseline()(crates/buzz-acp/src/usage.rs) seeds a zero baseline for sessions this process just spawned, so the first turn of a fresh session reports reliable token deltas instead of failing closed. Sessions the process re-attaches to are deliberately not seeded — their true prior cumulative is unknown, and fabricating a baseline there would misattribute another process's tokens to the first observed turn.last_costseeds toSome(0.0)so a first-turn cost delta computes against a real zero;last_totalstaysNonebecause NIP-AM forbids fabricating a total baseline for providers that never report one. The call sites inpool.rssit immediately aftercreate_session_and_apply_model()on both the channel and heartbeat paths, keyed offis_new_session— the only authoritative spawned-vs-attached signal. Seeding is idempotent viaentry().or_insert().Tests
store_migration_tests.rscovering M1 through the realopen_archive_dbpath (legacy-file backfill, idempotent reopen, crash-before-commit recovery).agentUsage.test.mjs/hooks.test.mjs: pure-helper unit tests for formatting, partial/unknown-field detection, compound-key sorting, custom-range validation, and local-midnight boundary construction across DST and skipped civil dates.tests/e2e/agent-usage.spec.ts: loading → resolved, preset and custom window switching, both click-through paths into the focused view, dated axis labels and on-bar values, tooltip contents (including unknown fields as unknown, not zero), error/retry, empty state, collection-off banner, partial badge rendering in row/bar/ingress for incomplete-provenance totals, focused-view unknown-intervals and invalid-reports caveats under their independent gate conditions, invalid-only window rendering the uncountable state in both overview and focused view (distinguished from ordinary-empty and outside-window), harness sub-label rendering, and the usage section's position below the agents and teams sections.crates/buzz-acp/src/usage.rs: regression tests covering the spawned first turn reporting reliable deltas, the re-attached first turn staying fail-closed, both paths going reliable by the second turn, every wire field on the first-turn payload, and the seed being a no-op when a baseline already exists.