Expand description
TelemetryHook — maps the agent’s lifecycle Event stream onto
structured tracing spans and events, so a run becomes observable in any
tracing subscriber.
Why tracing and not a hard OpenTelemetry dependency? Because tracing is
the idiomatic Rust instrumentation seam: the library emits spans + events,
and the binary chooses the exporter. Attach
tracing-opentelemetry with an
OTLP pipeline and every span below is exported to Jaeger / Tempo / any OTLP
backend with zero changes here; attach tracing_subscriber::fmt().json()
and you get newline-delimited JSON for log pipelines. One instrumentation,
many backends.
Field names follow the OpenTelemetry GenAI semantic conventions
(gen_ai.*), so any OTel-aware backend — Logfire, SigNoz, Langfuse via OTLP,
Grafana — recognizes token counts, model, and finish reason automatically and
computes cost/latency with zero mapping. The pre-convention flat names
(input_tokens, tool, …) are kept alongside as aliases for existing
consumers.
Span/event shape (target harness.telemetry):
agent_run (span, fields: source, gen_ai.operation.name=invoke_agent)
├─ run.start
├─ iter (iter)
├─ model.complete (gen_ai.operation.name=chat,
│ gen_ai.usage.input_tokens, gen_ai.usage.output_tokens,
│ gen_ai.usage.cached_input_tokens,
│ gen_ai.response.finish_reasons
│ + aliases: input_tokens, output_tokens,
│ cached_input_tokens, tool_calls, stop)
├─ tool.call (gen_ai.operation.name=execute_tool, gen_ai.tool.name,
│ ok, duration_ms + alias: tool)
├─ sensor (sensor, signals)
├─ compact (stage)
├─ budget.warning (ratio)
└─ run.endTo export over OTLP, enable the crate’s otel feature and call
[crate::otel::init_tracing_with_otlp] from your binary; see that module.
Wire it like any hook:
let loop_ = AgentLoop::new(model).with_hook(std::sync::Arc::new(TelemetryHook::new()));Structs§
- Telemetry
Hook - Emits a span per run and a structured event per model call, tool call, sensor, compaction, and budget warning. See the module docs for the OTLP bridge.