Skip to main content

neuron_runtime/
tracing_hook.rs

1//! Concrete [`ObservabilityHook`] using the [`tracing`] crate.
2//!
3//! Emits structured `tracing` events for each stage of the agentic loop.
4//! Wire to any `tracing`-compatible subscriber (`tracing-subscriber` for
5//! stdout, `tracing-opentelemetry` for OpenTelemetry export).
6
7use neuron_types::{HookAction, HookError, HookEvent, ObservabilityHook};
8
9/// An [`ObservabilityHook`] that emits structured [`tracing`] events.
10///
11/// Always returns [`HookAction::Continue`] — observes but never controls.
12///
13/// # Span levels
14///
15/// | Event | Level |
16/// |-------|-------|
17/// | LoopIteration, PreLlmCall, PostLlmCall, PreToolExecution, PostToolExecution | `DEBUG` |
18/// | ContextCompaction, SessionStart, SessionEnd | `INFO` |
19///
20/// # Example
21///
22/// ```no_run
23/// use neuron_runtime::TracingHook;
24///
25/// let hook = TracingHook::new();
26/// // Pass to AgentLoop::builder(...).hook(hook).build()
27/// ```
28pub struct TracingHook;
29
30impl TracingHook {
31    /// Create a new `TracingHook`.
32    #[must_use]
33    pub fn new() -> Self {
34        Self
35    }
36}
37
38impl Default for TracingHook {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl ObservabilityHook for TracingHook {
45    fn on_event(
46        &self,
47        event: HookEvent<'_>,
48    ) -> impl std::future::Future<Output = Result<HookAction, HookError>> + Send {
49        match &event {
50            HookEvent::LoopIteration { turn } => {
51                tracing::debug!(turn, "neuron.loop.iteration");
52            }
53            HookEvent::PreLlmCall { request } => {
54                tracing::debug!(
55                    model = %request.model,
56                    messages = request.messages.len(),
57                    tools = request.tools.len(),
58                    "neuron.llm.pre_call"
59                );
60            }
61            HookEvent::PostLlmCall { response } => {
62                tracing::debug!(
63                    model = %response.model,
64                    stop_reason = ?response.stop_reason,
65                    input_tokens = response.usage.input_tokens,
66                    output_tokens = response.usage.output_tokens,
67                    "neuron.llm.post_call"
68                );
69            }
70            HookEvent::PreToolExecution { tool_name, .. } => {
71                tracing::debug!(tool = %tool_name, "neuron.tool.pre_execution");
72            }
73            HookEvent::PostToolExecution { tool_name, output } => {
74                tracing::debug!(
75                    tool = %tool_name,
76                    is_error = output.is_error,
77                    "neuron.tool.post_execution"
78                );
79            }
80            HookEvent::ContextCompaction {
81                old_tokens,
82                new_tokens,
83            } => {
84                tracing::info!(
85                    old_tokens,
86                    new_tokens,
87                    reduced_by = old_tokens - new_tokens,
88                    "neuron.context.compaction"
89                );
90            }
91            HookEvent::SessionStart { session_id } => {
92                tracing::info!(session_id, "neuron.session.start");
93            }
94            HookEvent::SessionEnd { session_id } => {
95                tracing::info!(session_id, "neuron.session.end");
96            }
97        }
98        std::future::ready(Ok(HookAction::Continue))
99    }
100}