diff --git a/Cargo.lock b/Cargo.lock
index 29eb0c04..315feef0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1397,6 +1397,7 @@ dependencies = [
name = "hive-claude"
version = "0.1.0"
dependencies = [
+ "serde",
"serde_json",
"thiserror 2.0.18",
"tokio",
diff --git a/hive-ag3nt/src/events.rs b/hive-ag3nt/src/events.rs
index a9cf80ca..d9721b34 100644
--- a/hive-ag3nt/src/events.rs
+++ b/hive-ag3nt/src/events.rs
@@ -12,6 +12,7 @@ use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
+use hive_claude::TokenUsage;
use rusqlite::{Connection, params};
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast;
@@ -432,28 +433,6 @@ impl EventStore {
}
}
-/// Token usage emitted by claude in the final `result` stream-json event.
-/// All counts are in tokens. `None` fields mean the server didn't report them.
-#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
-pub struct TokenUsage {
- pub input_tokens: u64,
- pub output_tokens: u64,
- pub cache_read_input_tokens: u64,
- pub cache_creation_input_tokens: u64,
-}
-
-impl TokenUsage {
- /// Total context consumed this turn (input + cache reads + cache writes).
- /// This is the per-inference context footprint that counts against the
- /// model's `contextWindow` limit. Tracked from the last `assistant` event
- /// in the stream-json (per-inference usage, not the cumulative `result`
- /// event which sums across all inferences in a tool-heavy turn and can
- /// far exceed the per-inference window).
- #[must_use]
- pub fn context_tokens(&self) -> u64 {
- self.input_tokens + self.cache_read_input_tokens + self.cache_creation_input_tokens
- }
-}
/// Authoritative turn-loop state. The harness owns it; the web UI
/// reads via `/api/state` and renders. Lives alongside the bus
diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs
index 00451a9b..62661fbe 100644
--- a/hive-ag3nt/src/turn.rs
+++ b/hive-ag3nt/src/turn.rs
@@ -11,7 +11,7 @@ use anyhow::Result;
use hive_claude::{Config, InfiniteSession, PercentPolicy, Sink};
use serde_json::Value;
-use crate::events::{Bus, LiveEvent, TokenUsage};
+use crate::events::{Bus, LiveEvent};
use crate::mcp;
// Hive-enforced claude settings ship at `/etc/claude-code/managed-settings.json`
@@ -561,27 +561,13 @@ fn apply_telemetry(bus: &Bus, telemetry: &hive_claude::Telemetry) {
if telemetry.context.context_tokens() == 0 && telemetry.cost.context_tokens() == 0 {
return;
}
- bus.record_turn_usage(
- to_bus_usage(telemetry.context),
- to_bus_usage(telemetry.cost),
- );
+ bus.record_turn_usage(telemetry.context, telemetry.cost);
bus.set_resolved_model(telemetry.model.clone());
if let Some(window) = telemetry.context_window {
bus.set_api_context_window(window);
}
}
-/// Convert the lib's `TokenUsage` into the bus/stats `TokenUsage` (identical
-/// fields; the two crates keep their own types to avoid coupling).
-fn to_bus_usage(u: hive_claude::TokenUsage) -> TokenUsage {
- TokenUsage {
- input_tokens: u.input_tokens,
- output_tokens: u.output_tokens,
- cache_read_input_tokens: u.cache_read_input_tokens,
- cache_creation_input_tokens: u.cache_creation_input_tokens,
- }
-}
-
/// Archive (do NOT delete) the harness's own session so the next turn's
/// `--resume
` misses and self-heals into a fresh `--name `
/// session. Delegates the rename to [`hive_claude::SessionStore::archive_by_title`]
diff --git a/hive-ag3nt/src/turn_stats.rs b/hive-ag3nt/src/turn_stats.rs
index c239aff2..e314923e 100644
--- a/hive-ag3nt/src/turn_stats.rs
+++ b/hive-ag3nt/src/turn_stats.rs
@@ -268,8 +268,8 @@ impl TurnStats {
pub fn last_usage(
&self,
) -> (
- Option,
- Option,
+ Option,
+ Option,
) {
let conn = self.inner.lock().unwrap();
conn.query_row(
@@ -285,19 +285,19 @@ impl TurnStats {
let g = |i: usize| -> rusqlite::Result {
Ok(u64::try_from(row.get::<_, i64>(i)?).unwrap_or(0))
};
- let cost = crate::events::TokenUsage {
+ let cost = hive_claude::TokenUsage {
input_tokens: g(0)?,
output_tokens: g(1)?,
cache_read_input_tokens: g(2)?,
cache_creation_input_tokens: g(3)?,
};
- let last = crate::events::TokenUsage {
+ let last = hive_claude::TokenUsage {
input_tokens: g(4)?,
output_tokens: g(5)?,
cache_read_input_tokens: g(6)?,
cache_creation_input_tokens: g(7)?,
};
- let ctx = if last == crate::events::TokenUsage::default() {
+ let ctx = if last == hive_claude::TokenUsage::default() {
None
} else {
Some(last)
diff --git a/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs
index bbb9b889..0383ca0c 100644
--- a/hive-ag3nt/src/web_ui.rs
+++ b/hive-ag3nt/src/web_ui.rs
@@ -412,10 +412,10 @@ struct StateSnapshot {
/// Last-inference token usage from the most recent completed
/// turn — represents the current context-window size at turn-end.
/// `null` until the first turn finishes.
- ctx_usage: Option,
+ ctx_usage: Option,
/// Cumulative token usage across the most recent turn's inferences
/// (cost signal). `null` until the first turn finishes.
- cost_usage: Option,
+ cost_usage: Option,
/// Navigation links for this agent page. Also served via
/// `DashboardState.links` (`GET /api/dashboard-state`) for the
/// dashboard card's icon strip. Both are produced by `agent_links()`
diff --git a/hive-claude/Cargo.toml b/hive-claude/Cargo.toml
index 6218d201..63e6f608 100644
--- a/hive-claude/Cargo.toml
+++ b/hive-claude/Cargo.toml
@@ -7,6 +7,7 @@ version.workspace = true
workspace = true
[dependencies]
+serde = { workspace = true }
serde_json.workspace = true
thiserror.workspace = true
tokio.workspace = true
diff --git a/hive-claude/src/telemetry.rs b/hive-claude/src/telemetry.rs
index 3c1c490f..7bc62f5b 100644
--- a/hive-claude/src/telemetry.rs
+++ b/hive-claude/src/telemetry.rs
@@ -6,10 +6,11 @@
//! also want the raw events (for their own SSE / tool-call accounting) still
//! get them through their [`crate::Sink`].
+use serde::{Deserialize, Serialize};
use serde_json::Value;
/// Token counts from one `usage` block. All in tokens; missing fields read `0`.
-#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenUsage {
pub input_tokens: u64,
pub output_tokens: u64,
@@ -37,7 +38,7 @@ impl TokenUsage {
}
/// Everything the driver tracks from one turn's stream-json output.
-#[derive(Debug, Clone, Default, PartialEq, Eq)]
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Telemetry {
/// Most recent per-inference usage (from `assistant` events) — the live
/// context footprint (the number to watch for compaction).
@@ -108,7 +109,7 @@ fn context_window_from_result(event: &Value) -> Option {
/// The compaction signal: the live context size and the window it's measured
/// against. Derived from [`Telemetry`] via [`Telemetry::usage`].
-#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Usage {
/// Tokens in the last inference's context. `0` until the first `assistant`
/// event.