clawft-core 0.6.11

Core engine for clawft (agent loop, message bus, context management)
//! Lightweight chain event markers for crates without direct
//! [`ChainManager`](https://docs.rs/clawft-kernel) access.
//!
//! Since `clawft-core` does not depend on `clawft-kernel`, it cannot
//! call `ChainManager::append()` directly.  Instead, state-modifying
//! methods emit **structured tracing events** at `INFO` level using the
//! `chain_event!` macro.  The daemon layer (`clawft-weave`) subscribes
//! to the `chain_event` target and forwards matching spans to the real
//! ExoChain via `ChainManager::append()`.
//!
//! This design keeps `clawft-core` free of kernel dependencies while
//! still providing a consistent chain-logging surface.
//!
//! # Event kind constants
//!
//! These mirror the `EVENT_KIND_*` constants in `clawft-kernel::chain`
//! so that producers and consumers share a vocabulary without a compile
//! dependency.

/// Sandbox enforcement check.
pub const EVENT_KIND_SANDBOX_EXECUTE: &str = "sandbox.execute";

/// Session created.
pub const EVENT_KIND_SESSION_CREATE: &str = "session.create";

/// Session destroyed.
pub const EVENT_KIND_SESSION_DESTROY: &str = "session.destroy";

/// Workspace created.
pub const EVENT_KIND_WORKSPACE_CREATE: &str = "workspace.create";

/// Workspace config loaded / merged.
pub const EVENT_KIND_WORKSPACE_CONFIG: &str = "workspace.config";

/// Tool registered in the ToolRegistry.
pub const EVENT_KIND_TOOL_REGISTER: &str = "tool.register";

/// A pending chain event record for the tracing-to-ChainManager bridge.
///
/// Non-kernel crates cannot call `ChainManager::append()` directly.
/// Instead, they push records into the static [`PENDING_CHAIN_EVENTS`]
/// buffer via [`push_chain_event`]. The daemon periodically drains the
/// buffer via [`drain_pending_chain_events`] and forwards each record
/// to the real `ChainManager`.
#[derive(Debug, Clone)]
pub struct PendingChainEvent {
    /// Source subsystem (e.g. "session", "workspace", "wasm_fs").
    pub source: String,
    /// Event kind constant (e.g. "session.create").
    pub kind: String,
    /// Optional JSON payload.
    pub payload: Option<serde_json::Value>,
}

use std::sync::Mutex;

/// Global buffer of chain events emitted by non-kernel crates.
static PENDING_CHAIN_EVENTS: Mutex<Vec<PendingChainEvent>> = Mutex::new(Vec::new());

/// Push a chain event into the global pending buffer.
///
/// This is safe to call from any thread. The daemon drains this buffer
/// periodically and forwards events to `ChainManager::append()`.
pub fn push_chain_event(source: &str, kind: &str, payload: Option<serde_json::Value>) {
    if let Ok(mut buf) = PENDING_CHAIN_EVENTS.lock() {
        buf.push(PendingChainEvent {
            source: source.to_string(),
            kind: kind.to_string(),
            payload,
        });
    }
}

/// Drain all pending chain events from the global buffer.
///
/// Returns the accumulated events and clears the buffer. The daemon
/// calls this periodically to forward events to `ChainManager`.
pub fn drain_pending_chain_events() -> Vec<PendingChainEvent> {
    match PENDING_CHAIN_EVENTS.lock() {
        Ok(mut buf) => std::mem::take(&mut *buf),
        Err(_) => Vec::new(),
    }
}

/// Emit a structured tracing event on the `chain_event` target AND
/// push a record into the pending chain event buffer.
///
/// The daemon's chain event bridge drains the pending buffer and
/// forwards matching events to `ChainManager::append`.
///
/// # Usage
///
/// ```rust,ignore
/// chain_event!("session", "session.create", { "key": key });
/// ```
#[macro_export]
macro_rules! chain_event {
    ($source:expr, $kind:expr, { $($key:tt : $val:expr),* $(,)? }) => {
        {
            tracing::info!(
                target: "chain_event",
                source = $source,
                kind = $kind,
                $( $key = %$val, )*
                "chain"
            );
            $crate::chain_event::push_chain_event(
                $source,
                $kind,
                Some(serde_json::json!({ $( stringify!($key): format!("{}", $val) ),* })),
            );
        }
    };
}

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

    #[test]
    fn constants_match_expected_values() {
        assert_eq!(EVENT_KIND_SANDBOX_EXECUTE, "sandbox.execute");
        assert_eq!(EVENT_KIND_SESSION_CREATE, "session.create");
        assert_eq!(EVENT_KIND_SESSION_DESTROY, "session.destroy");
        assert_eq!(EVENT_KIND_WORKSPACE_CREATE, "workspace.create");
        assert_eq!(EVENT_KIND_WORKSPACE_CONFIG, "workspace.config");
        assert_eq!(EVENT_KIND_TOOL_REGISTER, "tool.register");
    }

    #[test]
    fn macro_compiles_and_pushes_event() {
        // Drain any stale events from previous tests.
        drain_pending_chain_events();

        // Verify the macro expands and pushes to the buffer.
        chain_event!("test", "test.event", { "foo": "bar" });

        let events = drain_pending_chain_events();
        assert!(!events.is_empty(), "macro should push to pending buffer");
        assert_eq!(events[0].source, "test");
        assert_eq!(events[0].kind, "test.event");
    }

    #[test]
    fn push_and_drain_chain_events() {
        drain_pending_chain_events();

        push_chain_event("src", "kind.test", Some(serde_json::json!({"a": 1})));
        push_chain_event("src2", "kind.test2", None);

        let events = drain_pending_chain_events();
        assert_eq!(events.len(), 2);
        assert_eq!(events[0].source, "src");
        assert_eq!(events[1].kind, "kind.test2");

        // Drain again should be empty.
        assert!(drain_pending_chain_events().is_empty());
    }
}