enact-core 0.0.2

Core agent runtime for Enact - Graph-Native AI agents
Documentation
//! Signal module - Optional, best-effort signaling for hints and notifications
//!
//! ## Important: Non-Authoritative
//!
//! This module provides **optional, best-effort signaling** for hints and
//! notifications. Signals are **not authoritative** and the kernel must work
//! correctly even if all signaling infrastructure is disabled.
//!
//! ## Three Kinds of "Messages"
//!
//! There are three distinct kinds of communication in the system:
//!
//! | Kind                       | Example                         | Where it belongs        |
//! | -------------------------- | ------------------------------- | ----------------------- |
//! | **Execution events**       | step started, decision made     | **Kernel only** (`kernel/event.rs`) |
//! | **Control signals**        | cancel execution, scale workers | **Control plane** (cloud) |
//! | **Infrastructure signals** | notify UI, wake worker          | **This module** (optional) |
//!
//! ## Signal Semantics
//!
//! Signals are:
//! - ✅ Non-authoritative (may be ignored)
//! - ✅ Best-effort (delivery not guaranteed)
//! - ✅ Optional (kernel works perfectly without them)
//! - ✅ For hints only (wake-up, notify, nudge)
//!
//! Signals are NOT:
//! - ❌ Execution events (use `kernel/event.rs`)
//! - ❌ State changes (use `kernel/reducer.rs`)
//! - ❌ Required for correctness
//! - ❌ Durable queues (use control plane)
//!
//! ## Invariant
//!
//! **The kernel must remain correct, deterministic, and replayable
//! even if all signaling infrastructure is disabled.**
//!
//! ## CRITICAL INVARIANT: Signals Never Drive Execution State
//!
//! SignalBus implementations MUST NOT have access to:
//! - ExecutionKernel
//! - Reducer
//! - ExecutionState
//!
//! Signals are hints only. If someone can "resume execution" via signal,
//! that is an architectural leak.
//!
//! **Enforcement**: SignalBus trait and implementations should not import
//! any kernel modules. If you find yourself needing kernel types in signal
//! code, you are violating this invariant.
//!
//! ## Use Cases
//!
//! ### Allowed Signal Uses
//! - ✅ Wake a paused execution
//! - ✅ Notify local UI
//! - ✅ Tell a worker "check for work"
//! - ✅ Hint that something changed
//!
//! ### Forbidden Signal Uses
//! - ❌ Carry execution events (use `kernel/event.rs`)
//! - ❌ Control execution state (use `kernel/reducer.rs`)
//! - ❌ Replace durable queues (use control plane)
//! - ❌ Drive orchestration logic (use control plane)
//!
//! ## Control Plane Messaging
//!
//! Authoritative messaging (Redis, Kafka, durable queues) lives in the
//! control plane (`apps/api/` or future `enact-control-plane` crate), not here.
//! The control plane:
//! - Implements durable queues
//! - Translates infra messages → kernel invocations
//! - Fans out kernel events to external systems
//!
//! The kernel does not know *how* messages travel. It only knows:
//! > "I was invoked."

use async_trait::async_trait;
use std::sync::Arc;
use tokio::sync::broadcast;

mod inmemory;

pub use inmemory::InMemorySignalBus;

/// Receiver for subscribed signals
pub type SignalReceiver<T> = broadcast::Receiver<T>;

/// SignalBus trait - Optional, best-effort signaling abstraction
///
/// This trait provides a non-authoritative signaling mechanism for hints
/// and notifications. Implementations should be lightweight and optional.
///
/// The kernel must work correctly even if SignalBus is disabled or fails.
#[async_trait]
pub trait SignalBus: Send + Sync {
    /// Emit a signal to a channel (best-effort, may be ignored)
    async fn emit(&self, channel: &str, signal: &[u8]) -> anyhow::Result<()>;

    /// Subscribe to a channel, returns a receiver for signals
    async fn subscribe(&self, channel: &str) -> anyhow::Result<SignalReceiver<Vec<u8>>>;

    /// Unsubscribe from a channel
    async fn unsubscribe(&self, channel: &str) -> anyhow::Result<()>;
}

/// Boxed SignalBus for dynamic dispatch
pub type DynSignalBus = Arc<dyn SignalBus>;