use harness_core::{Event, Hook, HookOutcome, World};
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::Instant;
pub struct TelemetryHook {
run: Mutex<Option<tracing::Span>>,
tool_starts: Mutex<HashMap<String, Instant>>,
}
impl TelemetryHook {
pub fn new() -> Self {
Self {
run: Mutex::new(None),
tool_starts: Mutex::new(HashMap::new()),
}
}
fn in_run<F: FnOnce()>(&self, f: F) {
let guard = self.run.lock().unwrap();
match &*guard {
Some(span) => span.in_scope(f),
None => f(),
}
}
}
impl Default for TelemetryHook {
fn default() -> Self {
Self::new()
}
}
impl Hook for TelemetryHook {
fn name(&self) -> &str {
"telemetry"
}
fn matches(&self, _ev: &Event<'_>) -> bool {
true
}
fn fire(&self, ev: &Event<'_>, _world: &mut World) -> HookOutcome {
match ev {
Event::SessionStart { source } => {
let span = tracing::info_span!(
target: "harness.telemetry",
"agent_run",
source = format!("{source:?}")
);
span.in_scope(|| {
tracing::info!(target: "harness.telemetry", event = "run.start");
});
*self.run.lock().unwrap() = Some(span);
}
Event::Heartbeat { iter } => self.in_run(|| {
tracing::info!(target: "harness.telemetry", event = "iter", iter = *iter);
}),
Event::PostModel { out } => self.in_run(|| {
tracing::info!(
target: "harness.telemetry",
event = "model.complete",
input_tokens = out.usage.input_tokens,
output_tokens = out.usage.output_tokens,
cached_input_tokens = out.usage.cached_input_tokens,
tool_calls = out.tool_calls.len(),
stop = format!("{:?}", out.stop_reason),
);
}),
Event::PreToolUse { action } => {
self.tool_starts
.lock()
.unwrap()
.insert(action.call_id.clone(), Instant::now());
}
Event::PostToolUse { action, result } => {
let duration_ms = self
.tool_starts
.lock()
.unwrap()
.remove(&action.call_id)
.map(|s| s.elapsed().as_millis() as u64)
.unwrap_or(0);
self.in_run(|| {
tracing::info!(
target: "harness.telemetry",
event = "tool.call",
tool = %action.tool,
ok = result.ok,
duration_ms,
);
});
}
Event::PostSensor { sensor, signals } => self.in_run(|| {
tracing::debug!(
target: "harness.telemetry",
event = "sensor",
sensor = %sensor,
signals = signals.len(),
);
}),
Event::PostCompact { stage } => self.in_run(|| {
tracing::debug!(
target: "harness.telemetry",
event = "compact",
stage = format!("{stage:?}"),
);
}),
Event::BudgetWarning { ratio } => self.in_run(|| {
tracing::warn!(
target: "harness.telemetry",
event = "budget.warning",
ratio = *ratio,
);
}),
Event::SessionEnd => {
self.in_run(|| {
tracing::info!(target: "harness.telemetry", event = "run.end");
});
*self.run.lock().unwrap() = None;
}
_ => {}
}
HookOutcome::Allow
}
}