codex-cli-sdk 0.0.1

Rust SDK for the OpenAI Codex CLI
Documentation
//! Event callback — a simple observe/filter hook for SDK consumers.
//!
//! The [`EventCallback`] is invoked for each [`ThreadEvent`](crate::ThreadEvent)
//! before it is yielded from the stream. It can:
//!
//! - **Observe** events (return `Some(event)` unchanged)
//! - **Transform** events (return `Some(modified_event)`)
//! - **Filter** events (return `None` to suppress)
//!
//! # Processing order
//!
//! The `EventCallback` runs **after** the hook system ([`crate::hooks`]).
//! Events that are blocked or aborted by a hook never reach the `EventCallback`.
//! The pipeline per event is:
//!
//! 1. [`crate::hooks::dispatch_hook`] — async, classified, first-match
//! 2. `EventCallback` — sync, raw event (only if the hook did not block/abort)
//!
//! # Example
//!
//! ```rust
//! use std::sync::Arc;
//! use codex_cli_sdk::callback::EventCallback;
//! use codex_cli_sdk::ThreadEvent;
//!
//! // Log all events, pass them through unchanged:
//! let logger: EventCallback = Arc::new(|event: ThreadEvent| {
//!     eprintln!("received: {event:?}");
//!     Some(event)
//! });
//!
//! // Filter out turn-started events:
//! let filter: EventCallback = Arc::new(|event: ThreadEvent| {
//!     match &event {
//!         ThreadEvent::TurnStarted => None,
//!         _ => Some(event),
//!     }
//! });
//! ```

use std::sync::Arc;

use crate::types::events::ThreadEvent;

/// Optional callback invoked for each event received from the CLI.
///
/// - Return `Some(event)` to pass the event through (possibly transformed).
/// - Return `None` to filter the event out of the stream.
///
/// When no callback is configured, all events pass through unchanged.
pub type EventCallback = Arc<dyn Fn(ThreadEvent) -> Option<ThreadEvent> + Send + Sync>;

/// Apply an event callback, or pass through if no callback is set.
#[inline]
pub fn apply_callback(event: ThreadEvent, callback: Option<&EventCallback>) -> Option<ThreadEvent> {
    match callback {
        Some(cb) => cb(event),
        None => Some(event),
    }
}

// ── Tests ────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    fn make_turn_started() -> ThreadEvent {
        ThreadEvent::TurnStarted
    }

    fn make_thread_started() -> ThreadEvent {
        ThreadEvent::ThreadStarted {
            thread_id: "t1".into(),
        }
    }

    #[test]
    fn no_callback_passes_through() {
        let event = make_turn_started();
        let result = apply_callback(event, None);
        assert!(result.is_some());
    }

    #[test]
    fn callback_can_filter() {
        let filter: EventCallback = Arc::new(|event| match &event {
            ThreadEvent::TurnStarted => None,
            _ => Some(event),
        });

        assert!(apply_callback(make_turn_started(), Some(&filter)).is_none());
        assert!(apply_callback(make_thread_started(), Some(&filter)).is_some());
    }

    #[test]
    fn callback_can_transform() {
        let transform: EventCallback = Arc::new(|event| {
            // Pass through but could modify here
            Some(event)
        });
        let event = make_thread_started();
        let result = apply_callback(event, Some(&transform));
        assert!(result.is_some());
    }

    #[test]
    fn callback_can_observe() {
        use std::sync::atomic::{AtomicUsize, Ordering};
        let count = Arc::new(AtomicUsize::new(0));
        let count_clone = Arc::clone(&count);

        let observer: EventCallback = Arc::new(move |event| {
            count_clone.fetch_add(1, Ordering::Relaxed);
            Some(event)
        });

        apply_callback(make_turn_started(), Some(&observer));
        apply_callback(make_thread_started(), Some(&observer));

        assert_eq!(count.load(Ordering::Relaxed), 2);
    }
}