feat(hive-claude): parse turn telemetry in the lib, return it from run
This commit is contained in:
parent
faa7f982af
commit
487e62a9ca
7 changed files with 260 additions and 227 deletions
|
|
@ -6,7 +6,6 @@
|
|||
//! compaction / auto-reset / retry state machine (`drive_turn`).
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use anyhow::Result;
|
||||
use hive_claude::{Config, InfiniteSession, PercentPolicy, Sink};
|
||||
|
|
@ -285,6 +284,9 @@ pub async fn drive_turn(prompt: &str, files: &TurnFiles, bus: &Bus) -> TurnOutco
|
|||
}
|
||||
let outcome = match result {
|
||||
Ok(progress) => {
|
||||
// Apply the turn's parsed usage / model / context-window to the bus
|
||||
// (badges, stats, auto-reset watermark input).
|
||||
apply_telemetry(bus, &progress.telemetry);
|
||||
if progress.created {
|
||||
// Fresh session minted this turn → flag it so the bin loop
|
||||
// mints a `sessions` row + stamps its id onto this turn's stats.
|
||||
|
|
@ -495,59 +497,27 @@ fn error_to_turn(err: hive_claude::Error) -> TurnOutcome {
|
|||
}
|
||||
}
|
||||
|
||||
/// Bridges a claude run's output stream onto the hyperhive event bus: parses
|
||||
/// per-turn token usage / resolved model / context-window from stream-json,
|
||||
/// mirrors every event to the SSE bus, and surfaces non-JSON stdout + stderr
|
||||
/// as Notes. Interior mutability (a `Mutex`) tracks the last inference across
|
||||
/// events; the driver calls the `Sink` methods synchronously from one reader
|
||||
/// task, so contention is nil — the lock only satisfies the `&self` trait
|
||||
/// signature (and keeps `BusSink: Sync` for the driver's `Send` future).
|
||||
/// Bridges a claude run's raw output stream onto the hyperhive event bus:
|
||||
/// per-turn tool-call counting (`observe_stream`), the live SSE stream, and
|
||||
/// non-JSON stdout + stderr as Notes. Stateless — usage/model/context-window
|
||||
/// parsing lives in `hive-claude` and is applied from the run's returned
|
||||
/// `Telemetry` (see `apply_telemetry`).
|
||||
struct BusSink<'a> {
|
||||
bus: &'a Bus,
|
||||
state: Mutex<BusSinkState>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct BusSinkState {
|
||||
last_inference: Option<TokenUsage>,
|
||||
last_model: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> BusSink<'a> {
|
||||
fn new(bus: &'a Bus) -> Self {
|
||||
Self {
|
||||
bus,
|
||||
state: Mutex::new(BusSinkState::default()),
|
||||
}
|
||||
Self { bus }
|
||||
}
|
||||
}
|
||||
|
||||
impl Sink for BusSink<'_> {
|
||||
fn on_event(&self, event: &Value) {
|
||||
{
|
||||
let mut st = self.state.lock().unwrap();
|
||||
// `last_inference` overwrites on every assistant event so at
|
||||
// result-time it holds the most recent model call's usage — the
|
||||
// actual context size. The `result` event carries the cumulative
|
||||
// cost usage; both update the badges together.
|
||||
if let Some(u) = TokenUsage::from_assistant_event(event) {
|
||||
st.last_inference = Some(u);
|
||||
}
|
||||
if let Some(m) = TokenUsage::model_from_assistant_event(event) {
|
||||
st.last_model = Some(m);
|
||||
}
|
||||
if let Some(cost) = TokenUsage::from_stream_event(event) {
|
||||
let ctx = st.last_inference.unwrap_or(cost);
|
||||
self.bus.record_turn_usage(ctx, cost);
|
||||
self.bus.set_resolved_model(st.last_model.clone());
|
||||
}
|
||||
}
|
||||
// Seed the API-reported context-window from the result event's
|
||||
// `modelUsage.*.contextWindow` — the authoritative active window for
|
||||
// compaction watermarks.
|
||||
if let Some(w) = TokenUsage::context_window_from_result_event(event) {
|
||||
self.bus.set_api_context_window(w);
|
||||
}
|
||||
// Raw-event concerns only: per-turn tool-call counting + the live SSE
|
||||
// stream. Usage / model / context-window parsing lives in the lib now
|
||||
// and is applied from the run's returned `Telemetry` (see `drive_turn`
|
||||
// → `apply_telemetry`).
|
||||
self.bus.observe_stream(event);
|
||||
self.bus.emit(LiveEvent::Stream(event.clone()));
|
||||
}
|
||||
|
|
@ -568,6 +538,36 @@ impl Sink for BusSink<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Apply a completed turn's parsed [`hive_claude::Telemetry`] to the bus:
|
||||
/// per-inference context usage + cumulative cost, the resolved model id, and
|
||||
/// the API-reported context window (the authoritative window for the auto-reset
|
||||
/// watermark). Skips a degenerate turn that parsed nothing so it doesn't reset
|
||||
/// the badges to zero.
|
||||
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.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 <title>` misses and self-heals into a fresh `--name <title>`
|
||||
/// session. Delegates the rename to [`hive_claude::SessionStore::archive_by_title`]
|
||||
|
|
|
|||
Loading…
Reference in a new issue