klieo-ops 0.41.2

Operational layer above klieo-core: supervisor, governor, gates, escalation, worklog, handoff.
Documentation
//! `OpsRuntime` builder.

use crate::clock::{SharedClock, SystemClock};
use crate::error::BuildError;
use crate::redactor::{DefaultRedactor, SharedRedactor};
use crate::runtime::supervised::SupervisedAgent;
use crate::tenant::TenantResolver;
use crate::types::{AgentId, ProviderId};
use klieo_core::Agent;
use std::sync::Arc;
use std::time::Duration;

/// Default approval timeout forwarded to [`GatedToolInvoker`].
#[cfg(feature = "gates")]
const DEFAULT_APPROVAL_TIMEOUT: Duration = Duration::from_secs(600);

/// Entry point for wiring the ops layer around a user-built agent.
pub struct OpsRuntime;

impl OpsRuntime {
    /// Start a builder chain.
    #[must_use]
    pub fn builder() -> OpsRuntimeBuilder {
        OpsRuntimeBuilder::default()
    }
}

/// Builder accumulator.
pub struct OpsRuntimeBuilder {
    tenant_resolver: Option<Arc<dyn TenantResolver>>,
    redactor: SharedRedactor,
    clock: SharedClock,
    agent_id: Option<AgentId>,
    #[cfg(feature = "governor")]
    governor: Option<Arc<dyn crate::governor::Governor>>,
    #[cfg(feature = "governor")]
    governor_provider: Option<ProviderId>,
    #[cfg(feature = "escalation")]
    escalation: Option<Arc<dyn crate::escalation::Escalation>>,
    #[cfg(feature = "gates")]
    gates: Vec<Arc<dyn crate::gates::Gate>>,
    #[cfg(feature = "gates")]
    approval_timeout: Duration,
    #[cfg(feature = "gates")]
    approval_timeout_policy: crate::gates::ApprovalTimeoutPolicy,
}

// Hand-written because DefaultRedactor and SystemClock construct non-trivial
// Arc values that cannot be expressed with #[derive(Default)].
impl Default for OpsRuntimeBuilder {
    fn default() -> Self {
        Self {
            tenant_resolver: None,
            redactor: Arc::new(DefaultRedactor::new()),
            clock: Arc::new(SystemClock::default()),
            agent_id: None,
            #[cfg(feature = "governor")]
            governor: None,
            #[cfg(feature = "governor")]
            governor_provider: None,
            #[cfg(feature = "escalation")]
            escalation: None,
            #[cfg(feature = "gates")]
            gates: Vec::new(),
            #[cfg(feature = "gates")]
            approval_timeout: DEFAULT_APPROVAL_TIMEOUT,
            #[cfg(feature = "gates")]
            approval_timeout_policy: crate::gates::ApprovalTimeoutPolicy::default(),
        }
    }
}

impl OpsRuntimeBuilder {
    /// Set the tenant resolver. Required before calling [`spawn`](Self::spawn).
    #[must_use]
    pub fn tenant_resolver(mut self, r: Arc<dyn TenantResolver>) -> Self {
        self.tenant_resolver = Some(r);
        self
    }

    /// Override the default redactor.
    #[must_use]
    pub fn redactor(mut self, r: SharedRedactor) -> Self {
        self.redactor = r;
        self
    }

    /// Override the default clock.
    #[must_use]
    pub fn clock(mut self, c: SharedClock) -> Self {
        self.clock = c;
        self
    }

    /// Override the agent identity recorded in
    /// [`OpsEvent::SupervisorStateChange`]. Defaults to the inner
    /// agent's [`Agent::name`] when not supplied.
    #[must_use]
    pub fn agent_id(mut self, id: AgentId) -> Self {
        self.agent_id = Some(id);
        self
    }

    /// Register a [`Governor`] that rate-limits LLM calls made by the
    /// wrapped agent.
    #[cfg(feature = "governor")]
    #[must_use]
    pub fn governor(mut self, g: Arc<dyn crate::governor::Governor>) -> Self {
        self.governor = Some(g);
        self
    }

    /// Set the provider tag recorded in [`OpsEvent::GovernorDenial`] events.
    /// Defaults to `"default"` when not supplied.
    #[cfg(feature = "governor")]
    #[must_use]
    pub fn governor_provider(mut self, p: ProviderId) -> Self {
        self.governor_provider = Some(p);
        self
    }

    /// Register an `Escalation` impl. Required when any registered gate
    /// may emit `RequireApproval`.
    #[cfg(feature = "escalation")]
    #[must_use]
    pub fn escalation(mut self, e: Arc<dyn crate::escalation::Escalation>) -> Self {
        self.escalation = Some(e);
        self
    }

    /// Register a [`Gate`] that guards every tool invocation made by the
    /// wrapped agent.
    #[cfg(feature = "gates")]
    #[must_use]
    pub fn gate(mut self, gate: Arc<dyn crate::gates::Gate>) -> Self {
        self.gates.push(gate);
        self
    }

    /// Override the approval timeout passed to [`GatedToolInvoker`].
    ///
    /// How long `GatedToolInvoker` waits for a gate's `wait_for_approval`
    /// to resolve before returning [`ToolError::Permanent`]. Defaults to
    /// 600 seconds (10 minutes) per spec § 2.3.
    #[cfg(feature = "gates")]
    #[must_use]
    pub fn approval_timeout(mut self, timeout: Duration) -> Self {
        self.approval_timeout = timeout;
        self
    }

    /// Override the policy applied when `wait_for_approval` times out.
    ///
    /// Defaults to `RequeueAndEscalate { max_requeues: 3 }` per spec § 2.3.
    /// Use `ApprovalTimeoutPolicy::Deny` to restore the pre-v0.4 behaviour
    /// (terminate immediately on the first timeout).
    #[cfg(feature = "gates")]
    #[must_use]
    pub fn approval_timeout_policy(mut self, policy: crate::gates::ApprovalTimeoutPolicy) -> Self {
        self.approval_timeout_policy = policy;
        self
    }

    /// Validate configuration and wrap the agent.
    ///
    /// Returns [`BuildError::MissingDependency`] when a required dependency
    /// is absent:
    /// - `tenant_resolver` was not supplied.
    /// - Any registered gate has `may_require_approval == true` but no
    ///   `Escalation` was configured.
    pub fn spawn<A>(self, agent: A) -> Result<SupervisedAgent<A>, BuildError>
    where
        A: Agent,
    {
        #[cfg(all(feature = "escalation", feature = "gates"))]
        if self.escalation.is_none() && self.gates.iter().any(|g| g.may_require_approval()) {
            return Err(BuildError::MissingDependency {
                needed_by: "Gate (may_require_approval == true)".into(),
                missing: "escalation".into(),
            });
        }

        let tenant_resolver =
            self.tenant_resolver
                .ok_or_else(|| BuildError::MissingDependency {
                    needed_by: "OpsRuntime".into(),
                    missing: "tenant_resolver".into(),
                })?;

        let agent_id = self
            .agent_id
            .unwrap_or_else(|| AgentId(agent.name().to_string()));

        // redactor and clock are load-bearing once telemetry / redaction hooks land.
        let _ = (&self.redactor, &self.clock);

        Ok(SupervisedAgent {
            inner: agent,
            tenant_resolver,
            agent_id,
            #[cfg(feature = "governor")]
            governor: self.governor,
            #[cfg(feature = "governor")]
            governor_provider: self
                .governor_provider
                .unwrap_or_else(|| ProviderId("default".into())),
            #[cfg(feature = "gates")]
            gates: self.gates,
            #[cfg(feature = "gates")]
            approval_timeout: self.approval_timeout,
            #[cfg(feature = "gates")]
            approval_timeout_policy: self.approval_timeout_policy,
        })
    }
}