rig_tap/lib.rs
1//! Emits uniform telemetry for [Rig](https://crates.io/crates/rig-core) agents
2//! and companion crates.
3//!
4//! `rig-tap` defines a stable, versioned [`ObservabilityEvent`] stream for
5//! prompt, tool, context, memory, and dispatch lifecycle events. Producer
6//! crates can use the same event vocabulary whether the event came from a Rig
7//! agent hook, `rig-compose` dispatch, `rig-memvid` memory behavior, or host
8//! application code.
9//!
10//! See the crate [README](../README.md) for the full schema and consumer
11//! recipe. The two consumer-facing types are:
12//!
13//! - [`TelemetryHook`] — implements [`rig::agent::PromptHook`] and emits
14//! `prompt.*` and `tool.*` events.
15//! - [`ObservedMemory`] — wraps any [`rig::memory::ConversationMemory`] and
16//! emits `context.sampled` on every load.
17//!
18//! Plus [`ChainedHook`] for composing two `PromptHook`s on a single agent.
19//!
20//! # Why rig-tap (vs. Rig's hooks today)
21//!
22//! Rig already exposes the raw callbacks and data: [`rig::agent::PromptHook`]
23//! (`on_completion_call` / `on_completion_response` / `on_tool_call` /
24//! `on_tool_result`), the `Usage` token counts on `CompletionResponse`, typed
25//! `PromptError` / `CompletionError` values, and GenAI span conventions. That
26//! is a callback surface scoped to one agent loop — ephemeral, provider-shaped,
27//! with no on-the-wire form. Nothing leaves the process, correlates across
28//! calls, or speaks a vocabulary other crates share unless you write that glue.
29//!
30//! `rig-tap` turns those callbacks into a stable, versioned, queryable
31//! telemetry contract:
32//!
33//! - **A versioned wire schema, not just callbacks** — every event is a flat,
34//! `serde`-stable [`ObservabilityEvent`] envelope. [`SCHEMA_VERSION`] +
35//! `#[non_exhaustive]` make additive evolution non-breaking.
36//! - **One vocabulary across the ecosystem** — the same [`EventKind`] covers
37//! agent prompts/tools, `rig-compose` kernel dispatch, memory/context, eval
38//! reports, and stateful provider sessions. `PromptHook` only sees the
39//! in-loop agent path.
40//! - **OTel-routable scalars** — each event surfaces [`ScalarFields`] as
41//! first-class `tracing` attributes (`model`, `tool_name`, `call_id`,
42//! `error_class`, …) plus `span_id` mirroring, so collectors route without
43//! parsing JSON.
44//! - **Lifecycle pairing** — a stable `call_id` pairs `tool.invoked` with its
45//! terminal event; `previous_response_id` chains stateful turns.
46//! - **Failure semantics** — [`ErrorClass`] normalizes provider errors into a
47//! backend-agnostic taxonomy with a `retriable` flag and HTTP status.
48//! - **Visibility the hooks lack** — provider-hosted tools (`tool.hosted_*`),
49//! latency milestones (`duration_ms`, `time_to_first_token_ms`), and
50//! Responses-WebSocket sessions (`response.*`) have no `PromptHook` analog.
51//! - **Operational plumbing** — pluggable [`SamplingPolicy`], payload
52//! truncation, an in-process [`EventQuery`], runtime-agnostic emission.
53//!
54//! `rig-tap` is **additive** to Rig's GenAI span conventions: its events live
55//! under a separate `tracing` target and can be filtered independently.
56//!
57//! # Wire format
58//!
59//! All events are emitted as a single `tracing::info!` event on the
60//! [`EVENT_TARGET`] target (`"rig_tap"`). The string field `event` carries the
61//! JSON-encoded [`ObservabilityEvent`], while scalar `rig_tap.*` fields expose
62//! `kind`, `conversation_id`, `version`, `tick`, and `occurred_at_millis` for
63//! OpenTelemetry collector routing and indexing without JSON parsing.
64//!
65//! # Subscriber sizing
66//!
67//! Emission is synchronous: every event runs serde + the registered
68//! layers on the calling task. Per-request hot paths (every prompt, every
69//! tool call, every memory load) call into the tracing dispatcher
70//! directly. For production deployments — especially ones that ship
71//! events off-host — wire a non-blocking sink (e.g.
72//! `tracing_appender::non_blocking` or a bounded channel feeding an
73//! async exporter) so a slow consumer can't backpressure the agent.
74//! In-process counters and the bundled doc-test layer are fine
75//! synchronous.
76//!
77//! # Example
78//!
79//! ```no_run
80//! use rig_tap::{TelemetryHook, ObservedMemory};
81//! use rig::memory::InMemoryConversationMemory;
82//!
83//! # fn build<M: rig::completion::CompletionModel>() -> TelemetryHook<M> {
84//! let memory = ObservedMemory::new(InMemoryConversationMemory::new());
85//! let hook = TelemetryHook::<M>::with_defaults("gpt-4o", "thread-1");
86//! // agent.memory(memory).with_hook(hook)
87//! # hook }
88//! ```
89
90#![deny(missing_docs)]
91
92pub mod extract;
93
94mod chained;
95#[cfg(feature = "compose")]
96mod dispatch;
97pub mod emit;
98mod error;
99mod event;
100mod hook;
101mod observed_memory;
102mod query;
103#[cfg(feature = "openai-responses")]
104pub mod responses_extract;
105#[cfg(all(feature = "openai-responses-websocket", not(target_family = "wasm")))]
106pub mod responses_session;
107mod sampling;
108#[cfg(feature = "subscriber")]
109mod subscriber;
110
111pub use chained::ChainedHook;
112#[cfg(feature = "compose")]
113pub use dispatch::DispatchObserveHook;
114pub use emit::{EVENT_TARGET, build_event, current_span_id, emit, emit_kind, try_emit};
115pub use error::Error;
116pub use event::{
117 ErrorClass, EventKind, ObservabilityEvent, PAYLOAD_TRUNCATE_BYTES, SCHEMA_VERSION,
118 ScalarFields, truncate_utf8,
119};
120pub use extract::extract_event;
121pub use hook::{
122 ConversationIdResolver, ModelResolver, PreviousResponseIdResolver, TelemetryHook,
123 TelemetryHookConfig,
124};
125pub use observed_memory::ObservedMemory;
126pub use query::{EventFilter, EventQuery};
127#[cfg(feature = "openai-responses")]
128pub use responses_extract::{HostedToolCall, emit_hosted_tools, extract_hosted_tools};
129#[cfg(all(feature = "openai-responses-websocket", not(target_family = "wasm")))]
130pub use responses_session::{ObservedResponsesSession, ResponsesSessionObserver};
131pub use sampling::{AlwaysSample, RatePolicy, SamplingPolicy};
132#[cfg(feature = "subscriber")]
133pub use subscriber::CapturingLayer;