lifeloop-cli 0.5.0

Provider-neutral lifecycle abstraction and normalizer for AI harnesses
Documentation
//! Trait seams for the router stages.
//!
//! These traits (introduced with the router skeleton in issue #7)
//! name the integration shape; each is now implemented by a concrete
//! type in a sibling module, re-exported through `super` (`mod.rs`):
//!
//! * [`CallbackInvoker`] — invoke a client callback for a routed
//!   event. Implemented by `DefaultCallbackInvoker` (in-process,
//!   `callbacks.rs`) and `SubprocessCallbackInvoker` (process
//!   boundary, `subprocess.rs`). The trait method signature takes a
//!   [`RoutingPlan`] plus opaque [`PayloadEnvelope`]s so an
//!   implementation can destructure without this module growing a
//!   `protocol` dependency.
//! * [`ReceiptEmitter`] — persist or forward the
//!   [`crate::LifecycleReceipt`] produced by a dispatch. Implemented by
//!   `LifeloopReceiptEmitter` in `receipts.rs`.
//! * [`FailureMapper`] — turn a [`super::RouteError`] (or a
//!   downstream stage's error) into the [`crate::FailureClass`] /
//!   [`crate::RetryClass`] pair the receipt carries. Implemented by
//!   `LifeloopFailureMapper` in `failure_mapping.rs`.

use crate::{CallbackResponse, FailureClass, LifecycleReceipt, PayloadEnvelope, RetryClass};

use super::plan::RoutingPlan;
use super::validation::RouteError;

/// Callback invocation seam.
///
/// Implemented by `DefaultCallbackInvoker` (in-process, `callbacks.rs`)
/// and `SubprocessCallbackInvoker` (process boundary, `subprocess.rs`).
/// The signature takes a [`RoutingPlan`] plus the optional
/// [`PayloadEnvelope`]s the caller is delivering and lets the
/// implementation destructure, so the seam stays free of any
/// `protocol` dependency.
///
/// The [`PayloadEnvelope::body`] is treated as opaque bytes/value —
/// implementations must not parse it.
pub trait CallbackInvoker {
    /// Error produced by the callback transport. Kept generic so the
    /// in-process and subprocess impls (and any future network impl)
    /// can each carry their own diagnostic shape.
    type Error;

    /// Invoke the client callback for a routed event.
    ///
    /// `payloads` carries any [`PayloadEnvelope`]s the caller is
    /// delivering alongside the event. The router does not inspect
    /// payload bodies; the invoker may, but is not required to.
    fn invoke(
        &self,
        plan: &RoutingPlan,
        payloads: &[PayloadEnvelope],
    ) -> Result<CallbackResponse, Self::Error>;
}

/// Receipt emission seam.
///
/// `LifeloopReceiptEmitter` (in `receipts.rs`) synthesizes the
/// [`crate::LifecycleReceipt`] and routes it through an idempotency
/// store; this trait method validates and stores an already-built
/// receipt.
pub trait ReceiptEmitter {
    /// Error produced by the receipt sink.
    type Error;

    /// Emit (persist, forward, or otherwise hand off) a
    /// [`LifecycleReceipt`].
    fn emit(&self, receipt: &LifecycleReceipt) -> Result<(), Self::Error>;
}

/// Failure-class mapping seam.
///
/// `LifeloopFailureMapper` (in `failure_mapping.rs`) maps
/// [`RouteError`] (and downstream stage errors) onto the
/// [`FailureClass`] / [`RetryClass`] pair carried on a `status=failed`
/// receipt.
pub trait FailureMapper {
    /// Map a [`RouteError`] to a `(failure_class, retry_class)`
    /// pair. Implementations must be deterministic — the same
    /// [`RouteError`] variant must always map to the same pair so
    /// receipt ledgers replay consistently.
    fn map_route_error(&self, err: &RouteError) -> (FailureClass, RetryClass);
}