agents: live token meter — accumulate per-task usage across both providers #952

Closed
opened 2026-05-08 12:04:54 +00:00 by claude-desktop · 2 comments
Collaborator

User story

As an operator running expensive agents under a hard monthly budget cap (€200 extra-usage), I want a live per-task token meter that ticks up as the agent works, regardless of provider, so I can interrupt runaway tasks before they burn the budget.

Context

Today every observability tile reads zero for cursor runs because cursor-sdk-adapter.ts hardcodes usage: emptyUsage() and totalCostUsd: 0. Claude Code surfaces token counts only on the terminal result event (no live ticker). The user has explicitly stripped [1m] everywhere because of past budget overrun (per global memory feedback_one_m_context.md) — token visibility is load-bearing for cost control, not just UI polish.

This issue assumes the foundation issue (cursor InteractionUpdate deltas — usage_delta event added to the port) has landed, or implements the minimum amount of that scaffolding needed for the meter to work.

Acceptance criteria

Port

  • UsageDeltaEvent { type: "usage_delta", sessionId, deltaInput, deltaOutput, deltaCacheRead?, deltaCacheCreation? }. Emitted incrementally as the agent runs.

Cursor adapter

  • Subscribe to TokenDeltaUpdate via the interaction listener. Translate to UsageDeltaEvent.

Claude adapter

  • Synthesize UsageDeltaEvent per assistant turn from assistant.message.usage (post-turn deltas; not as fine-grained as cursor but still live).

Runner / event log

  • Maintain a per-task running total in the TaskRecord (record.usage = { input, output, cache_read, cache_creation }); update on each usage_delta.
  • Broadcast a coalesced usage_delta SSE envelope at most every 250 ms per task to avoid spamming the wire on rapid streams.
  • Persist final usage into task_history (already wired for result.totalUsage); this issue keeps that invariant — the meter is a display of in-progress state, the row is still authoritative.

Frontend

  • New small <TokenMeter task={...} /> component. Renders inline tokens-in / tokens-out, optionally cache-read / cache-creation. Updates live via SSE.
  • Mount it in: task drawer (planner board side panel), monitor row, agent timeline header. Compact form on cards, full form in detail.
  • Color scheme: at <50% of soft budget = neutral, 50–90% = warn, >90% = error. Soft budget configurable in settings.

Tests

  • Adapter tests for both providers asserting UsageDeltaEvent cadence + correctness.
  • Runner test asserting accumulation across deltas matches the terminal result.totalUsage.
  • Frontend snapshot for the meter component at idle / mid / over-budget states.

Out of scope

  • Cost computation in USD/EUR — that's the cost-rate-table issue. This issue ships token counts only.
  • Budget enforcement (auto-cancel above threshold) — separate ops issue.

References

  • Parent: #950
  • Foundation: cursor InteractionUpdate delta issue
  • Cursor token type: delta-types.d.ts TokenDeltaUpdate
  • Memory: feedback_one_m_context.md
## User story As an operator running expensive agents under a hard monthly budget cap (€200 extra-usage), I want a live per-task token meter that ticks up as the agent works, regardless of provider, so I can interrupt runaway tasks before they burn the budget. ## Context Today every observability tile reads zero for cursor runs because `cursor-sdk-adapter.ts` hardcodes `usage: emptyUsage()` and `totalCostUsd: 0`. Claude Code surfaces token counts only on the terminal `result` event (no live ticker). The user has explicitly stripped `[1m]` everywhere because of past budget overrun (per global memory `feedback_one_m_context.md`) — token visibility is load-bearing for cost control, not just UI polish. This issue assumes the foundation issue (cursor `InteractionUpdate` deltas — `usage_delta` event added to the port) has landed, or implements the minimum amount of that scaffolding needed for the meter to work. ## Acceptance criteria ### Port - [ ] `UsageDeltaEvent { type: "usage_delta", sessionId, deltaInput, deltaOutput, deltaCacheRead?, deltaCacheCreation? }`. Emitted incrementally as the agent runs. ### Cursor adapter - [ ] Subscribe to `TokenDeltaUpdate` via the interaction listener. Translate to `UsageDeltaEvent`. ### Claude adapter - [ ] Synthesize `UsageDeltaEvent` per assistant turn from `assistant.message.usage` (post-turn deltas; not as fine-grained as cursor but still live). ### Runner / event log - [ ] Maintain a per-task running total in the `TaskRecord` (`record.usage = { input, output, cache_read, cache_creation }`); update on each `usage_delta`. - [ ] Broadcast a coalesced `usage_delta` SSE envelope at most every 250 ms per task to avoid spamming the wire on rapid streams. - [ ] Persist final usage into `task_history` (already wired for `result.totalUsage`); this issue keeps that invariant — the meter is a *display* of in-progress state, the row is still authoritative. ### Frontend - [ ] New small `<TokenMeter task={...} />` component. Renders inline tokens-in / tokens-out, optionally cache-read / cache-creation. Updates live via SSE. - [ ] Mount it in: task drawer (planner board side panel), monitor row, agent timeline header. Compact form on cards, full form in detail. - [ ] Color scheme: at <50% of soft budget = neutral, 50–90% = warn, >90% = error. Soft budget configurable in settings. ### Tests - [ ] Adapter tests for both providers asserting `UsageDeltaEvent` cadence + correctness. - [ ] Runner test asserting accumulation across deltas matches the terminal `result.totalUsage`. - [ ] Frontend snapshot for the meter component at idle / mid / over-budget states. ## Out of scope - Cost computation in USD/EUR — that's the cost-rate-table issue. This issue ships token counts only. - Budget enforcement (auto-cancel above threshold) — separate ops issue. ## References - Parent: #950 - Foundation: cursor `InteractionUpdate` delta issue - Cursor token type: `delta-types.d.ts` `TokenDeltaUpdate` - Memory: `feedback_one_m_context.md`
Collaborator

🤖 Auto-assigned to code-lead (heuristic: area:agents → code-lead (architecture-touching)). Reply /unassign to reroute.

🤖 Auto-assigned to **code-lead** (heuristic: area:agents → code-lead (architecture-touching)). Reply `/unassign` to reroute.
Collaborator

🧹 janitor: this ticket has been idle-assigned since 2026-05-08T15:36:16.000Z. Re-dispatching.

🧹 janitor: this ticket has been idle-assigned since 2026-05-08T15:36:16.000Z. Re-dispatching.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
charles/claude-hooks#952
No description provided.