Skip to main content

lc_callbacks/
base.rs

1// lc-callbacks/src/base.rs
2//! Base callback handler trait
3
4use async_trait::async_trait;
5use std::collections::HashMap;
6use std::sync::Arc;
7use tokio::sync::Mutex;
8
9use super::run_tree::RunTree;
10use crate::tracing::{SpanGuard, SpanKind, Tracer};
11use lc_schema::Message;
12
13/// Callback handler trait for tracing and monitoring
14///
15/// Implement this trait to receive callbacks during execution.
16/// Can be used for logging, tracing, monitoring, etc.
17///
18/// # Two information layers
19///
20/// The trait is split into two layers:
21///
22/// 1. **Generic lifecycle layer** — [`on_run_start`](CallbackHandler::on_run_start),
23///    [`on_run_end`](CallbackHandler::on_run_end) and
24///    [`on_run_error`](CallbackHandler::on_run_error) carry the run's lifecycle
25///    with only the [`RunTree`] (name, type, inputs/outputs, timing). They are
26///    type-agnostic: a chain, an LLM call, a tool and a retriever all funnel
27///    through the same three hooks.
28/// 2. **Typed payload layer** — `on_llm_*`, `on_chain_*`, `on_tool_*` and
29///    `on_retriever_*` additionally receive the type-specific payload
30///    (messages, tokens, tool name, query, …). Their default implementations
31///    delegate to the generic layer, so a handler that only cares about
32///    lifecycle may override just the three generic methods, while a handler
33///    that needs typed data overrides the typed ones.
34///
35/// Handlers should prefer overriding the typed methods when they need more than
36/// lifecycle signals; the built-in [`FileCallbackHandler`](crate::FileCallbackHandler)
37/// and [`GenericHandler`](crate::GenericHandler) do exactly that.
38#[async_trait]
39pub trait CallbackHandler: Send + Sync {
40    // ============ Lifecycle callbacks ============
41
42    /// Called when any run starts
43    async fn on_run_start(&self, run: &RunTree);
44
45    /// Called when a run ends successfully
46    async fn on_run_end(&self, run: &RunTree);
47
48    /// Called when a run fails
49    async fn on_run_error(&self, run: &RunTree, error: &str);
50
51    // ============ LLM callbacks ============
52
53    /// Called when an LLM starts
54    async fn on_llm_start(&self, run: &RunTree, _messages: &[Message]) {
55        self.on_run_start(run).await;
56    }
57
58    /// Called when an LLM ends
59    async fn on_llm_end(&self, run: &RunTree, _response: &str) {
60        self.on_run_end(run).await;
61    }
62
63    /// Called for each new token during streaming
64    async fn on_llm_new_token(&self, _run: &RunTree, _token: &str) {
65        // Default: do nothing
66    }
67
68    /// Called for each thinking token during streaming (extended thinking).
69    ///
70    /// Anthropic's extended thinking feature emits thinking content blocks
71    /// before the final text response. This callback fires for each chunk
72    /// of thinking content, allowing consumers to observe the model's
73    /// reasoning process in real time.
74    async fn on_llm_thinking(&self, _run: &RunTree, _thinking: &str) {
75        // Default: do nothing
76    }
77
78    /// Called when an LLM errors
79    async fn on_llm_error(&self, run: &RunTree, error: &str) {
80        self.on_run_error(run, error).await;
81    }
82
83    // ============ Chain callbacks ============
84
85    /// Called when a chain starts
86    async fn on_chain_start(&self, run: &RunTree, _inputs: &serde_json::Value) {
87        self.on_run_start(run).await;
88    }
89
90    /// Called when a chain ends
91    async fn on_chain_end(&self, run: &RunTree, _outputs: &serde_json::Value) {
92        self.on_run_end(run).await;
93    }
94
95    /// Called when a chain errors
96    async fn on_chain_error(&self, run: &RunTree, error: &str) {
97        self.on_run_error(run, error).await;
98    }
99
100    // ============ Tool callbacks ============
101
102    /// Called when a tool starts
103    async fn on_tool_start(&self, run: &RunTree, _tool_name: &str, _input: &str) {
104        self.on_run_start(run).await;
105    }
106
107    /// Called when a tool ends
108    async fn on_tool_end(&self, run: &RunTree, _output: &str) {
109        self.on_run_end(run).await;
110    }
111
112    /// Called when a tool errors
113    async fn on_tool_error(&self, run: &RunTree, error: &str) {
114        self.on_run_error(run, error).await;
115    }
116
117    // ============ Retriever callbacks ============
118
119    /// Called when a retriever starts
120    async fn on_retriever_start(&self, run: &RunTree, _query: &str) {
121        self.on_run_start(run).await;
122    }
123
124    /// Called when a retriever ends
125    async fn on_retriever_end(&self, run: &RunTree, _documents: &[serde_json::Value]) {
126        self.on_run_end(run).await;
127    }
128
129    /// Called when a retriever errors
130    async fn on_retriever_error(&self, run: &RunTree, error: &str) {
131        self.on_run_error(run, error).await;
132    }
133}
134
135/// Callback manager that handles multiple handlers
136///
137/// Dispatch events to all registered handlers. Prefer the `dispatch_*` methods
138/// over poking `handlers()` directly: the dispatch methods guarantee every
139/// handler is invoked in order and (when a [`Tracer`] is attached via
140/// [`with_tracer`](CallbackManager::with_tracer)) also publish each run as a
141/// tracing span.
142pub struct CallbackManager {
143    inner: Arc<CallbackManagerInner>,
144    tracer: Option<Arc<Tracer>>,
145    active_trace_spans: Arc<Mutex<HashMap<String, SpanGuard>>>,
146}
147
148struct CallbackManagerInner {
149    handlers: Vec<Arc<dyn CallbackHandler>>,
150}
151
152impl std::fmt::Debug for CallbackManager {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        f.debug_struct("CallbackManager")
155            .field("handlers_count", &self.inner.handlers.len())
156            .field("tracer_attached", &self.tracer.is_some())
157            .finish()
158    }
159}
160
161impl CallbackManager {
162    /// Create a new callback manager
163    pub fn new() -> Self {
164        Self {
165            inner: Arc::new(CallbackManagerInner {
166                handlers: Vec::new(),
167            }),
168            tracer: None,
169            active_trace_spans: Arc::new(Mutex::new(HashMap::new())),
170        }
171    }
172
173    /// Add a callback handler
174    pub fn add_handler(self, handler: Arc<dyn CallbackHandler>) -> Self {
175        let mut handlers = self.inner.handlers.clone();
176        handlers.push(handler);
177        Self {
178            inner: Arc::new(CallbackManagerInner { handlers }),
179            tracer: self.tracer,
180            active_trace_spans: self.active_trace_spans,
181        }
182    }
183
184    /// Attach a [`Tracer`] so every dispatched run is also published as a
185    /// tracing span (Q6: minimal merge between callbacks and the tracing tree).
186    ///
187    /// With a tracer attached, `dispatch_*_start` starts a span named after the
188    /// run and `dispatch_*_end`/`dispatch_*_error` ends it. Nested runs become
189    /// parent/child spans via the tracer's task-local span stack.
190    pub fn with_tracer(self, tracer: Arc<Tracer>) -> Self {
191        Self {
192            inner: self.inner,
193            tracer: Some(tracer),
194            active_trace_spans: self.active_trace_spans,
195        }
196    }
197
198    /// Get all handlers
199    pub fn handlers(&self) -> &[Arc<dyn CallbackHandler>] {
200        &self.inner.handlers
201    }
202
203    /// Check if there are any handlers
204    pub fn is_empty(&self) -> bool {
205        self.inner.handlers.is_empty()
206    }
207
208    /// Merge `other`'s handlers into a new manager, keeping this manager's
209    /// handlers first and appending `other`'s (Q8: config merges append
210    /// callback handlers instead of replacing them wholesale).
211    ///
212    /// `self`'s tracer wins; if `self` has none, `other`'s tracer is used.
213    /// The active trace-span table starts empty in the merged manager.
214    pub fn merge_with(&self, other: &CallbackManager) -> CallbackManager {
215        let mut handlers = self.inner.handlers.clone();
216        handlers.extend(other.inner.handlers.iter().cloned());
217        let tracer = self.tracer.clone().or_else(|| other.tracer.clone());
218        CallbackManager {
219            inner: Arc::new(CallbackManagerInner { handlers }),
220            tracer,
221            active_trace_spans: Arc::new(Mutex::new(HashMap::new())),
222        }
223    }
224
225    // ---- Tracing-span bridge (Q6) ----
226
227    /// Publish `run` start as a tracing span when a tracer is attached.
228    async fn begin_trace_span(&self, run: &RunTree) {
229        if let Some(tracer) = &self.tracer {
230            let kind = SpanKind::from(run.run_type);
231            // start_child falls back to a root span when no span is active
232            let guard = tracer.start_child(&run.name, kind);
233            self.active_trace_spans
234                .lock()
235                .await
236                .insert(run.id.to_string(), guard);
237        }
238    }
239
240    /// End the tracing span for `run` (marking it errored if `error` is set).
241    async fn end_trace_span(&self, run: &RunTree, error: Option<&str>) {
242        if let Some(mut guard) = self
243            .active_trace_spans
244            .lock()
245            .await
246            .remove(&run.id.to_string())
247        {
248            if let Some(msg) = error {
249                guard.set_error(msg);
250            }
251            guard.end();
252        }
253    }
254}
255
256impl Default for CallbackManager {
257    fn default() -> Self {
258        Self::new()
259    }
260}
261
262impl Clone for CallbackManager {
263    fn clone(&self) -> Self {
264        Self {
265            inner: Arc::clone(&self.inner),
266            tracer: self.tracer.clone(),
267            active_trace_spans: Arc::clone(&self.active_trace_spans),
268        }
269    }
270}
271
272// ============ Helper methods for dispatching callbacks ============
273
274impl CallbackManager {
275    /// Dispatch `on_run_start` to all handlers.
276    pub async fn dispatch_run_start(&self, run: &RunTree) {
277        self.begin_trace_span(run).await;
278        for handler in &self.inner.handlers {
279            handler.on_run_start(run).await;
280        }
281    }
282
283    /// Dispatch `on_run_end` to all handlers.
284    pub async fn dispatch_run_end(&self, run: &RunTree) {
285        self.end_trace_span(run, None).await;
286        for handler in &self.inner.handlers {
287            handler.on_run_end(run).await;
288        }
289    }
290
291    /// Dispatch `on_run_error` to all handlers.
292    pub async fn dispatch_run_error(&self, run: &RunTree, error: &str) {
293        self.end_trace_span(run, Some(error)).await;
294        for handler in &self.inner.handlers {
295            handler.on_run_error(run, error).await;
296        }
297    }
298
299    /// Dispatch `on_chain_start` to all handlers.
300    pub async fn dispatch_chain_start(&self, run: &RunTree, inputs: &serde_json::Value) {
301        self.begin_trace_span(run).await;
302        for handler in &self.inner.handlers {
303            handler.on_chain_start(run, inputs).await;
304        }
305    }
306
307    /// Dispatch `on_chain_end` to all handlers.
308    pub async fn dispatch_chain_end(&self, run: &RunTree, outputs: &serde_json::Value) {
309        self.end_trace_span(run, None).await;
310        for handler in &self.inner.handlers {
311            handler.on_chain_end(run, outputs).await;
312        }
313    }
314
315    /// Dispatch `on_chain_error` to all handlers.
316    pub async fn dispatch_chain_error(&self, run: &RunTree, error: &str) {
317        self.end_trace_span(run, Some(error)).await;
318        for handler in &self.inner.handlers {
319            handler.on_chain_error(run, error).await;
320        }
321    }
322
323    /// Dispatch `on_llm_start` to all handlers.
324    pub async fn dispatch_llm_start(&self, run: &RunTree, messages: &[lc_schema::Message]) {
325        self.begin_trace_span(run).await;
326        for handler in &self.inner.handlers {
327            handler.on_llm_start(run, messages).await;
328        }
329    }
330
331    /// Dispatch `on_llm_end` to all handlers.
332    pub async fn dispatch_llm_end(&self, run: &RunTree, response: &str) {
333        self.end_trace_span(run, None).await;
334        for handler in &self.inner.handlers {
335            handler.on_llm_end(run, response).await;
336        }
337    }
338
339    /// Dispatch `on_llm_error` to all handlers.
340    pub async fn dispatch_llm_error(&self, run: &RunTree, error: &str) {
341        self.end_trace_span(run, Some(error)).await;
342        for handler in &self.inner.handlers {
343            handler.on_llm_error(run, error).await;
344        }
345    }
346
347    /// Dispatch `on_llm_new_token` to all handlers.
348    pub async fn dispatch_llm_new_token(&self, run: &RunTree, token: &str) {
349        for handler in &self.inner.handlers {
350            handler.on_llm_new_token(run, token).await;
351        }
352    }
353
354    /// Dispatch `on_llm_thinking` to all handlers.
355    pub async fn dispatch_llm_thinking(&self, run: &RunTree, thinking: &str) {
356        for handler in &self.inner.handlers {
357            handler.on_llm_thinking(run, thinking).await;
358        }
359    }
360
361    /// Dispatch `on_tool_start` to all handlers.
362    pub async fn dispatch_tool_start(&self, run: &RunTree, tool_name: &str, input: &str) {
363        self.begin_trace_span(run).await;
364        for handler in &self.inner.handlers {
365            handler.on_tool_start(run, tool_name, input).await;
366        }
367    }
368
369    /// Dispatch `on_tool_end` to all handlers.
370    pub async fn dispatch_tool_end(&self, run: &RunTree, output: &str) {
371        self.end_trace_span(run, None).await;
372        for handler in &self.inner.handlers {
373            handler.on_tool_end(run, output).await;
374        }
375    }
376
377    /// Dispatch `on_tool_error` to all handlers.
378    pub async fn dispatch_tool_error(&self, run: &RunTree, error: &str) {
379        self.end_trace_span(run, Some(error)).await;
380        for handler in &self.inner.handlers {
381            handler.on_tool_error(run, error).await;
382        }
383    }
384
385    /// Dispatch `on_retriever_start` to all handlers.
386    pub async fn dispatch_retriever_start(&self, run: &RunTree, query: &str) {
387        self.begin_trace_span(run).await;
388        for handler in &self.inner.handlers {
389            handler.on_retriever_start(run, query).await;
390        }
391    }
392
393    /// Dispatch `on_retriever_end` to all handlers.
394    pub async fn dispatch_retriever_end(&self, run: &RunTree, documents: &[serde_json::Value]) {
395        self.end_trace_span(run, None).await;
396        for handler in &self.inner.handlers {
397            handler.on_retriever_end(run, documents).await;
398        }
399    }
400
401    /// Dispatch `on_retriever_error` to all handlers.
402    pub async fn dispatch_retriever_error(&self, run: &RunTree, error: &str) {
403        self.end_trace_span(run, Some(error)).await;
404        for handler in &self.inner.handlers {
405            handler.on_retriever_error(run, error).await;
406        }
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413    use crate::tracing::InMemoryTracingBackend;
414
415    struct RecordingHandler {
416        events: Mutex<Vec<String>>,
417    }
418
419    #[async_trait]
420    impl CallbackHandler for RecordingHandler {
421        async fn on_run_start(&self, run: &RunTree) {
422            self.events.lock().await.push(format!("start:{}", run.name));
423        }
424        async fn on_run_end(&self, run: &RunTree) {
425            self.events.lock().await.push(format!("end:{}", run.name));
426        }
427        async fn on_run_error(&self, _run: &RunTree, error: &str) {
428            self.events.lock().await.push(format!("error:{}", error));
429        }
430    }
431
432    #[tokio::test]
433    async fn test_dispatch_run_start_end() {
434        let handler = Arc::new(RecordingHandler {
435            events: Mutex::new(Vec::new()),
436        });
437        let cm = CallbackManager::new().add_handler(handler.clone());
438
439        let run = RunTree::new("r1", crate::RunType::Chain, serde_json::json!({}));
440        cm.dispatch_run_start(&run).await;
441        cm.dispatch_run_end(&run).await;
442
443        let events = handler.events.lock().await.clone();
444        assert_eq!(events, vec!["start:r1".to_string(), "end:r1".to_string()]);
445    }
446
447    #[tokio::test]
448    async fn test_dispatch_run_error_forwards_message() {
449        let handler = Arc::new(RecordingHandler {
450            events: Mutex::new(Vec::new()),
451        });
452        let cm = CallbackManager::new().add_handler(handler.clone());
453
454        let run = RunTree::new("r1", crate::RunType::Tool, serde_json::json!({}));
455        cm.dispatch_run_start(&run).await;
456        cm.dispatch_run_error(&run, "boom").await;
457
458        let events = handler.events.lock().await.clone();
459        assert!(events.contains(&"error:boom".to_string()));
460    }
461
462    #[tokio::test]
463    async fn test_dispatch_llm_thinking_forwards() {
464        struct ThinkingHandler {
465            received: Mutex<Vec<String>>,
466        }
467        #[async_trait]
468        impl CallbackHandler for ThinkingHandler {
469            async fn on_run_start(&self, _run: &RunTree) {}
470            async fn on_run_end(&self, _run: &RunTree) {}
471            async fn on_run_error(&self, _run: &RunTree, _e: &str) {}
472            async fn on_llm_thinking(&self, _run: &RunTree, thinking: &str) {
473                self.received.lock().await.push(thinking.to_string());
474            }
475        }
476        let handler = Arc::new(ThinkingHandler {
477            received: Mutex::new(Vec::new()),
478        });
479        let cm = CallbackManager::new().add_handler(handler.clone());
480
481        let run = RunTree::new("llm1", crate::RunType::Llm, serde_json::json!({}));
482        cm.dispatch_llm_start(&run, &[]).await;
483        cm.dispatch_llm_thinking(&run, "let me think").await;
484        cm.dispatch_llm_end(&run, "answer").await;
485
486        let received = handler.received.lock().await.clone();
487        assert_eq!(received, vec!["let me think".to_string()]);
488    }
489
490    #[tokio::test]
491    async fn test_with_tracer_publishes_run_spans() {
492        let backend = Arc::new(InMemoryTracingBackend::new());
493        let tracer = Arc::new(Tracer::new(backend.clone()));
494
495        let cm = CallbackManager::new().with_tracer(tracer);
496        let run = RunTree::new("span_run", crate::RunType::Chain, serde_json::json!({}));
497
498        cm.dispatch_run_start(&run).await;
499        cm.dispatch_run_end(&run).await;
500
501        let spans = backend.spans();
502        assert_eq!(spans.len(), 1);
503        assert_eq!(spans[0].name, "span_run");
504        assert_eq!(spans[0].kind, SpanKind::Chain);
505    }
506
507    #[tokio::test]
508    async fn test_with_tracer_publishes_error_status() {
509        let backend = Arc::new(InMemoryTracingBackend::new());
510        let tracer = Arc::new(Tracer::new(backend.clone()));
511
512        let cm = CallbackManager::new().with_tracer(tracer);
513        let run = RunTree::new("fail_run", crate::RunType::Llm, serde_json::json!({}));
514
515        cm.dispatch_llm_start(&run, &[]).await;
516        cm.dispatch_llm_error(&run, "model timeout").await;
517
518        let spans = backend.spans();
519        assert_eq!(spans.len(), 1);
520        assert!(matches!(
521            &spans[0].status,
522            crate::tracing::SpanStatus::Error(e) if e == "model timeout"
523        ));
524    }
525
526    #[tokio::test]
527    async fn test_nested_runs_become_parent_child_spans() {
528        let backend = Arc::new(InMemoryTracingBackend::new());
529        let tracer = Arc::new(Tracer::new(backend.clone()));
530
531        let cm = CallbackManager::new().with_tracer(tracer);
532
533        let chain = RunTree::new("outer_chain", crate::RunType::Chain, serde_json::json!({}));
534        cm.dispatch_chain_start(&chain, &serde_json::json!({}))
535            .await;
536
537        let llm = RunTree::new("inner_llm", crate::RunType::Llm, serde_json::json!({}));
538        cm.dispatch_llm_start(&llm, &[]).await;
539        cm.dispatch_llm_end(&llm, "answer").await;
540
541        cm.dispatch_chain_end(&chain, &serde_json::json!({})).await;
542
543        let spans = backend.spans();
544        assert_eq!(spans.len(), 2);
545        let outer = spans.iter().find(|s| s.name == "outer_chain").unwrap();
546        let inner = spans.iter().find(|s| s.name == "inner_llm").unwrap();
547        assert!(outer.parent_id.is_none());
548        assert_eq!(inner.parent_id.as_deref(), Some(outer.id.as_str()));
549    }
550}