Skip to main content

ratel_ai_core/trace/
event.rs

1use serde::{Deserialize, Serialize};
2
3/// Distinguishes a direct API call (pre-fetch helpers, library callers,
4/// benchmarks) from one the agent synthesized inside its loop (capability tool).
5/// Used to separate the two paths in trace consumers (rerankers train on agent
6/// calls, inspector shows both).
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum Origin {
10    /// A direct API call — SDK helpers, library callers, benchmarks. Wire
11    /// value `direct`.
12    Direct,
13    /// A call the agent synthesized inside its loop, via the capability
14    /// tools. Wire value `agent`.
15    Agent,
16}
17
18/// How a registry corpus changed — carried by [`TraceEvent::IndexChurn`]
19/// (tools) and [`TraceEvent::SkillChurn`] (skills).
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum ChurnKind {
23    /// An item was registered — including a replace-in-place re-register of
24    /// an existing id. Wire value `add`.
25    Add,
26    /// An item was removed from the corpus. Wire value `remove`.
27    Remove,
28}
29
30/// Outcome of the one-time embedding-model load. `Slow` flags a machine that may
31/// be underpowered for the model; `Failed` a load that errored (network, cache,
32/// corrupt weights).
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum EmbedderLoadStatus {
36    /// The model loaded within the expected budget. Wire value `ok`.
37    Ok,
38    /// The model loaded, but slowly — the machine may be underpowered for it.
39    /// Wire value `slow`.
40    Slow,
41    /// The load errored (network, cache, corrupt weights); the accompanying
42    /// `reason` carries the error. Wire value `failed`.
43    Failed,
44}
45
46/// One ranked tool hit inside a [`TraceEvent::Search`] event.
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48pub struct SearchHitTrace {
49    /// Id of the matching tool.
50    pub tool_id: String,
51    /// The engine score, widened to `f64` — same per-method semantics as
52    /// [`crate::SearchHit::score`].
53    pub score: f64,
54}
55
56/// Timing and top score of one engine stage of a search. BM25 searches emit
57/// one `bm25` stage, semantic searches one `dense` stage; hybrid emits
58/// `bm25`, `dense`, and `rrf`, in that order. Semantic and hybrid searches
59/// that short-circuit on an empty corpus or `top_k == 0` emit no stages.
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61pub struct SearchStage {
62    /// Stage name: `"bm25"`, `"dense"`, or `"rrf"`.
63    pub name: String,
64    /// Stage wall time, in milliseconds.
65    pub took_ms: u64,
66    /// Best score the stage produced (that stage's scale); `None` when it
67    /// returned no hits.
68    pub top_score: Option<f64>,
69}
70
71/// One ranked skill hit inside a [`TraceEvent::SkillSearch`] event — the
72/// skill-side twin of [`SearchHitTrace`].
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74pub struct SkillHitTrace {
75    /// Id of the matching skill.
76    pub skill_id: String,
77    /// The engine score, widened to `f64` — same per-method semantics as
78    /// [`crate::SkillHit::score`].
79    pub score: f64,
80}
81
82/// Every event produced by any layer of Ratel. New variants are additive;
83/// renames or removals are breaking — see ADR-0007.
84///
85/// On the wire each event is a JSON object whose `type` tag is the variant
86/// name in snake_case (`IndexChurn` → `index_churn`), with the variant's
87/// fields flattened beside it; sinks wrap it in a [`TraceEnvelope`]. All
88/// `took_ms` fields are wall time in milliseconds.
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90#[serde(tag = "type", rename_all = "snake_case")]
91pub enum TraceEvent {
92    /// A [`crate::ToolRegistry`] search completed (any [`crate::SearchMethod`]).
93    /// Carries the query, the requested `top_k`, the ranked `hits` with
94    /// scores, the per-engine `stages` timings, and the total wall time.
95    Search {
96        /// The search text.
97        query: String,
98        /// Direct library call vs agent-synthesized.
99        origin: Origin,
100        /// Requested result count.
101        top_k: u32,
102        /// The ranked results, best-first.
103        hits: Vec<SearchHitTrace>,
104        /// Per-engine stage timings (`bm25` / `dense` / `rrf`).
105        stages: Vec<SearchStage>,
106        /// Total search wall time, in milliseconds.
107        took_ms: u64,
108    },
109    /// The tool corpus changed: [`crate::ToolRegistry::register`] emits this
110    /// with [`ChurnKind::Add`] for both a fresh registration and a
111    /// replace-in-place re-register.
112    IndexChurn {
113        /// Whether the id was added or removed.
114        kind: ChurnKind,
115        /// Id of the affected tool.
116        tool_id: String,
117    },
118    /// A [`crate::SkillRegistry`] search completed — the skill-side twin of
119    /// [`TraceEvent::Search`], with the same shape.
120    SkillSearch {
121        /// The search text.
122        query: String,
123        /// Direct library call vs agent-synthesized.
124        origin: Origin,
125        /// Requested result count.
126        top_k: u32,
127        /// The ranked results, best-first.
128        hits: Vec<SkillHitTrace>,
129        /// Per-engine stage timings (`bm25` / `dense` / `rrf`).
130        stages: Vec<SearchStage>,
131        /// Total search wall time, in milliseconds.
132        took_ms: u64,
133    },
134    /// The skill corpus changed — the skill-side twin of
135    /// [`TraceEvent::IndexChurn`], emitted by [`crate::SkillRegistry::register`].
136    SkillChurn {
137        /// Whether the id was added or removed.
138        kind: ChurnKind,
139        /// Id of the affected skill.
140        skill_id: String,
141    },
142    /// A skill's body was loaded for dispatch (the `get_skill_content` path).
143    /// Emitted by the SDK skill catalogs via
144    /// [`crate::SkillRegistry::record_event`].
145    SkillInvoke {
146        /// Id of the loaded skill.
147        skill_id: String,
148        /// Load wall time, in milliseconds.
149        took_ms: u64,
150    },
151    /// A tool invocation began. Emitted by the SDK catalogs just before the
152    /// tool's executor runs; paired with [`TraceEvent::InvokeEnd`] or
153    /// [`TraceEvent::InvokeError`].
154    InvokeStart {
155        /// Id of the invoked tool.
156        tool_id: String,
157        /// Size of the serialized argument payload, in bytes.
158        args_size_bytes: u64,
159    },
160    /// A tool invocation completed successfully.
161    InvokeEnd {
162        /// Id of the invoked tool.
163        tool_id: String,
164        /// Invocation wall time, in milliseconds.
165        took_ms: u64,
166    },
167    /// A tool invocation failed; `error` carries the executor's message.
168    InvokeError {
169        /// Id of the invoked tool.
170        tool_id: String,
171        /// Wall time until the failure, in milliseconds.
172        took_ms: u64,
173        /// The failure message.
174        error: String,
175    },
176    /// The agent searched the catalog through the capability tools
177    /// (`search_capabilities`, or the deprecated `search_tools`). Carries only
178    /// the hit *count*; the ranked list with scores is on the underlying
179    /// [`TraceEvent::Search`] / [`TraceEvent::SkillSearch`] the registries
180    /// emit for the same call. The `gateway_*` wire prefix is frozen
181    /// (ADR-0007: renames are breaking).
182    GatewaySearch {
183        /// The search text.
184        query: String,
185        /// Direct library call vs agent-synthesized.
186        origin: Origin,
187        /// Requested result count.
188        top_k: u32,
189        /// Number of results returned.
190        hits: u32,
191        /// Total search wall time, in milliseconds.
192        took_ms: u64,
193    },
194    /// The agent invoked a tool through the `invoke_tool` capability tool and
195    /// it succeeded.
196    GatewayInvoke {
197        /// Id of the invoked tool.
198        tool_id: String,
199        /// Invocation wall time, in milliseconds.
200        took_ms: u64,
201    },
202    /// A capability-tool call failed: an unknown tool/skill id, an executor
203    /// error, or an upstream that needs auth.
204    GatewayError {
205        /// Id of the tool (or skill) the call named.
206        tool_id: String,
207        /// The failure message (e.g. `needs_auth`).
208        error: String,
209    },
210    /// An upstream MCP server's tools were ingested into the catalog
211    /// (the SDK's `register_mcp_server`).
212    UpstreamRegister {
213        /// Upstream server name.
214        server: String,
215        /// Transport used to reach it (e.g. `stdio` / `http` / `sse`).
216        transport: String,
217        /// Number of tools ingested.
218        tool_count: u32,
219    },
220    /// A proxied call to a tool backed by an upstream MCP server completed.
221    UpstreamInvoke {
222        /// Upstream server name.
223        server: String,
224        /// Id of the invoked tool.
225        tool_id: String,
226        /// Invocation wall time, in milliseconds.
227        took_ms: u64,
228    },
229    /// A proxied upstream call failed; `error` carries the upstream's message.
230    UpstreamError {
231        /// Upstream server name.
232        server: String,
233        /// Id of the invoked tool.
234        tool_id: String,
235        /// The failure message.
236        error: String,
237    },
238    /// A credential refresh for an upstream MCP server was attempted.
239    AuthRefresh {
240        /// Upstream server name.
241        upstream: String,
242        /// Whether the refresh produced valid credentials.
243        ok: bool,
244    },
245    /// An upstream MCP server challenged for auth (e.g. a 401): user
246    /// interaction is required before its tools work.
247    AuthNeeds {
248        /// Upstream server name.
249        upstream: String,
250    },
251    /// An interactive auth flow (e.g. OAuth) started for an upstream MCP
252    /// server; paired with [`TraceEvent::AuthFlowEnd`].
253    AuthFlowStart {
254        /// Upstream server name.
255        upstream: String,
256    },
257    /// The interactive auth flow ended.
258    AuthFlowEnd {
259        /// Upstream server name.
260        upstream: String,
261        /// Whether the flow produced valid credentials.
262        ok: bool,
263    },
264    /// Emitted once, on the first (cold) load of the embedding model. `status`
265    /// flags a slow load (possibly underpowered machine) or a failed one;
266    /// `reason` carries the hint / error. See `embedding.rs` and ADR-0011.
267    EmbedderLoad {
268        /// Resolved model display name: repo id, local path, or endpoint model
269        /// and URL.
270        model: String,
271        /// Load outcome: ok, slow, or failed.
272        status: EmbedderLoadStatus,
273        /// Load wall time, in milliseconds (`0` when the load failed before
274        /// timing).
275        took_ms: u64,
276        /// The slow-load hint or the load error; `None` on a normal load.
277        reason: Option<String>,
278    },
279    /// Emitted once when a configured embedding model is actually downloaded to
280    /// the HuggingFace cache (a cold fetch), carrying the real byte size — so a
281    /// multi-second first-run download is never a silent surprise. See ADR-0012.
282    EmbedderDownload {
283        /// The model that was downloaded.
284        model: String,
285        /// Real download size, in bytes.
286        bytes: u64,
287    },
288    /// Emitted when a semantic/hybrid search runs against an embedding set built
289    /// with a *different* model than the one now configured. Retrieval fails
290    /// rather than mixing vector spaces; the caller must rebuild the complete
291    /// embedding cache. See `dense_cache.rs` and ADR-0012.
292    EmbedderModelMismatch {
293        /// The model the existing embeddings were built with.
294        built: String,
295        /// The model now configured.
296        active: String,
297    },
298    /// Emitted once when an in-process model's pooling could not be detected
299    /// (no `1_Pooling/config.json`) and no override was given, so a mode was
300    /// assumed. A non-silent guess: set `pooling` to correct it. See ADR-0012.
301    EmbedderPoolingAssumed {
302        /// The model whose pooling could not be detected.
303        model: String,
304        /// The pooling mode that was assumed (`"cls"` or `"mean"`).
305        pooling: String,
306    },
307}
308
309/// The versioned wrapper a sink writes around each [`TraceEvent`]: schema
310/// version, timestamp, and session id. On the wire the event is flattened
311/// (`#[serde(flatten)]`), so its `type` tag and fields sit beside `v` / `ts` /
312/// `session_id` in one JSON object.
313#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
314pub struct TraceEnvelope {
315    /// Envelope schema version; currently `1`.
316    pub v: u32,
317    /// Event time, in milliseconds since the Unix epoch.
318    pub ts: u64,
319    /// The session the event belongs to, as given to the sink — correlates
320    /// all events from one agent session.
321    pub session_id: String,
322    /// The event itself, flattened into the envelope on the wire.
323    #[serde(flatten)]
324    pub event: TraceEvent,
325}