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}