nanny-core 0.6.1

Core traits and types for nanny, the authorization and audit layer for AI agents
Documentation
use serde::{Deserialize, Serialize};

/// Every possible reason execution was stopped.
///
/// This enum is exhaustive by design. Adding a new stop condition requires
/// adding a variant here first: the compiler will then force every match
/// site to handle it. That is the point.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum StopReason {
    /// The agent attempted to call a tool not on the allowlist.
    /// Carries the name of the denied tool for audit purposes.
    ToolDenied { tool_name: String },

    /// A custom rule returned a denial verdict.
    /// Carries the rule name so the audit log identifies exactly which rule fired.
    RuleDenied { rule_name: String },

    /// Execution was stopped explicitly by the caller.
    ManualStop,

    /// The agent declared itself complete.
    /// This is a successful, normal termination: not a constraint violation.
    AgentCompleted,
}

/// The state machine for a single agent execution.
///
/// Transitions are strictly one-way:
///   Initialized → Running → Stopped
///                         → Failed
///
/// Once in a terminal state (Stopped or Failed), execution cannot
/// resume or transition further. Attempting to do so is always a bug.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionState {
    /// Execution has been configured but not yet started.
    Initialized,

    /// Execution is actively running.
    Running,

    /// Execution ended with an explicit, typed reason.
    /// This is the normal terminal state.
    Stopped { reason: StopReason },

    /// Execution ended due to an unrecoverable internal error.
    /// Distinct from Stopped: this is abnormal termination.
    Failed { error: String },
}

impl ExecutionState {
    /// Returns true if this state is terminal.
    ///
    /// A terminal state must never transition to any other state.
    /// If the execution loop checks this and finds true, it must stop immediately.
    pub fn is_terminal(&self) -> bool {
        matches!(
            self,
            ExecutionState::Stopped { .. } | ExecutionState::Failed { .. }
        )
    }
}