Skip to main content

codex_cli_sdk/
callback.rs

1//! Event callback — a simple observe/filter hook for SDK consumers.
2//!
3//! The [`EventCallback`] is invoked for each [`ThreadEvent`](crate::ThreadEvent)
4//! before it is yielded from the stream. It can:
5//!
6//! - **Observe** events (return `Some(event)` unchanged)
7//! - **Transform** events (return `Some(modified_event)`)
8//! - **Filter** events (return `None` to suppress)
9//!
10//! # Processing order
11//!
12//! The `EventCallback` runs **after** the hook system ([`crate::hooks`]).
13//! Events that are blocked or aborted by a hook never reach the `EventCallback`.
14//! The pipeline per event is:
15//!
16//! 1. [`crate::hooks::dispatch_hook`] — async, classified, first-match
17//! 2. `EventCallback` — sync, raw event (only if the hook did not block/abort)
18//!
19//! # Example
20//!
21//! ```rust
22//! use std::sync::Arc;
23//! use codex_cli_sdk::callback::EventCallback;
24//! use codex_cli_sdk::ThreadEvent;
25//!
26//! // Log all events, pass them through unchanged:
27//! let logger: EventCallback = Arc::new(|event: ThreadEvent| {
28//!     eprintln!("received: {event:?}");
29//!     Some(event)
30//! });
31//!
32//! // Filter out turn-started events:
33//! let filter: EventCallback = Arc::new(|event: ThreadEvent| {
34//!     match &event {
35//!         ThreadEvent::TurnStarted => None,
36//!         _ => Some(event),
37//!     }
38//! });
39//! ```
40
41use std::sync::Arc;
42
43use crate::types::events::ThreadEvent;
44
45/// Optional callback invoked for each event received from the CLI.
46///
47/// - Return `Some(event)` to pass the event through (possibly transformed).
48/// - Return `None` to filter the event out of the stream.
49///
50/// When no callback is configured, all events pass through unchanged.
51pub type EventCallback = Arc<dyn Fn(ThreadEvent) -> Option<ThreadEvent> + Send + Sync>;
52
53/// Apply an event callback, or pass through if no callback is set.
54#[inline]
55pub fn apply_callback(event: ThreadEvent, callback: Option<&EventCallback>) -> Option<ThreadEvent> {
56    match callback {
57        Some(cb) => cb(event),
58        None => Some(event),
59    }
60}
61
62// ── Tests ────────────────────────────────────────────────────────────────────
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    fn make_turn_started() -> ThreadEvent {
69        ThreadEvent::TurnStarted
70    }
71
72    fn make_thread_started() -> ThreadEvent {
73        ThreadEvent::ThreadStarted {
74            thread_id: "t1".into(),
75        }
76    }
77
78    #[test]
79    fn no_callback_passes_through() {
80        let event = make_turn_started();
81        let result = apply_callback(event, None);
82        assert!(result.is_some());
83    }
84
85    #[test]
86    fn callback_can_filter() {
87        let filter: EventCallback = Arc::new(|event| match &event {
88            ThreadEvent::TurnStarted => None,
89            _ => Some(event),
90        });
91
92        assert!(apply_callback(make_turn_started(), Some(&filter)).is_none());
93        assert!(apply_callback(make_thread_started(), Some(&filter)).is_some());
94    }
95
96    #[test]
97    fn callback_can_transform() {
98        let transform: EventCallback = Arc::new(|event| {
99            // Pass through but could modify here
100            Some(event)
101        });
102        let event = make_thread_started();
103        let result = apply_callback(event, Some(&transform));
104        assert!(result.is_some());
105    }
106
107    #[test]
108    fn callback_can_observe() {
109        use std::sync::atomic::{AtomicUsize, Ordering};
110        let count = Arc::new(AtomicUsize::new(0));
111        let count_clone = Arc::clone(&count);
112
113        let observer: EventCallback = Arc::new(move |event| {
114            count_clone.fetch_add(1, Ordering::Relaxed);
115            Some(event)
116        });
117
118        apply_callback(make_turn_started(), Some(&observer));
119        apply_callback(make_thread_started(), Some(&observer));
120
121        assert_eq!(count.load(Ordering::Relaxed), 2);
122    }
123}