klieo-ops 3.5.0

Operational layer above klieo-core: supervisor, governor, gates, escalation, worklog, handoff.
Documentation
//! Shared types used across klieo-ops primitives.

use serde::{Deserialize, Serialize};
use std::fmt;
use std::time::Duration;

/// Stable, persistence-safe agent identifier. Distinct from in-process
/// handle. Two restarts of the same agent share an `AgentId`.
#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct AgentId(pub String);

impl fmt::Display for AgentId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

/// Stable, persistence-safe tenant identifier. Used for multi-tenant
/// audit scoping. Always part of the canonical signed event payload.
#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct TenantId(pub String);

impl fmt::Display for TenantId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

/// LLM provider identifier. Free-form string for forward compatibility.
#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct ProviderId(pub String);

impl fmt::Display for ProviderId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

/// Per-agent metadata registered with the supervisor.
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct AgentMeta {
    /// Stable id.
    pub id: AgentId,
    /// Logical role (e.g. `claims-triage`).
    pub role: String,
    /// Build version of the agent code.
    pub version: String,
    /// Ed25519 public key for agent-identity signatures (Phase B Handoff).
    /// Phase A stores a 32-byte slice; identity verification activates with
    /// Handoff (Phase B).
    pub identity_pubkey: [u8; 32],
    /// Expected per-step p99 latency. Influences supervisor stall threshold:
    /// `max(15s, 3 × expected_step_p99)`. `None` ⇒ defaults to 30 s.
    pub expected_step_p99: Option<Duration>,
}

/// Process-wide runtime lifecycle state.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum RuntimeState {
    /// Normal operation.
    #[default]
    Running,
    /// Draining in-flight work toward `Halted`.
    Draining,
    /// Refusing new step invocations; kill-switch tripped.
    Halted,
}

impl fmt::Display for RuntimeState {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Running => f.write_str("running"),
            Self::Draining => f.write_str("draining"),
            Self::Halted => f.write_str("halted"),
        }
    }
}

/// Scope of a governor budget assignment.
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum BudgetScope {
    /// Cluster-wide.
    Global,
    /// Per-tenant.
    Tenant(TenantId),
    /// Per-provider.
    Provider(ProviderId),
    /// Per-tenant × per-provider.
    TenantProvider(TenantId, ProviderId),
    /// Per-agent.
    Agent(AgentId),
}

impl fmt::Display for BudgetScope {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Global => f.write_str("global"),
            Self::Tenant(t) => write!(f, "tenant:{t}"),
            Self::Provider(p) => write!(f, "provider:{p}"),
            Self::TenantProvider(t, p) => write!(f, "tenant:{t}:provider:{p}"),
            Self::Agent(a) => write!(f, "agent:{a}"),
        }
    }
}

/// Reason a kill-switch was tripped.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct KillReason(pub String);

/// Source of a kill-switch trip.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub enum KillTrigger {
    /// Programmatic decision by the supervisor.
    Programmatic,
    /// External operator signal / HTTP / control plane.
    External(String),
}

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

    #[test]
    fn agent_id_round_trips_through_string() {
        let a = AgentId("claims-triage".into());
        let s = serde_json::to_string(&a).unwrap();
        let b: AgentId = serde_json::from_str(&s).unwrap();
        assert_eq!(a, b);
    }

    #[test]
    fn runtime_state_default_is_running() {
        assert_eq!(RuntimeState::default(), RuntimeState::Running);
    }

    #[test]
    fn budget_scope_formats_distinctly() {
        let g = BudgetScope::Global;
        let t = BudgetScope::Tenant(TenantId("bl_auto".into()));
        let p = BudgetScope::Provider(ProviderId("anthropic".into()));
        assert_ne!(format!("{g}"), format!("{t}"));
        assert_ne!(format!("{t}"), format!("{p}"));
    }
}