Skip to main content

klieo_ops/runtime/
builder.rs

1//! `OpsRuntime` builder.
2
3use crate::clock::{SharedClock, SystemClock};
4use crate::error::BuildError;
5use crate::redactor::{DefaultRedactor, SharedRedactor};
6use crate::runtime::supervised::SupervisedAgent;
7use crate::tenant::{SingleTenant, TenantResolver};
8use crate::types::TenantId;
9use crate::types::{AgentId, ProviderId};
10use klieo_core::Agent;
11use std::sync::Arc;
12#[cfg(feature = "gates")]
13use std::time::Duration;
14
15/// Default approval timeout forwarded to [`GatedToolInvoker`].
16#[cfg(feature = "gates")]
17const DEFAULT_APPROVAL_TIMEOUT: Duration = Duration::from_secs(600);
18
19/// Entry point for wiring the ops layer around a user-built agent.
20pub struct OpsRuntime;
21
22impl OpsRuntime {
23    /// Start a builder chain.
24    #[must_use]
25    pub fn builder() -> OpsRuntimeBuilder {
26        OpsRuntimeBuilder::default()
27    }
28
29    /// Capability-shaped default for single-tenant deployments — start
30    /// a builder chain with a [`SingleTenant`] resolver pre-wired.
31    ///
32    /// Power users wiring a real [`TenantResolver`] (read tenant from a
33    /// request-scoped task-local, JWT claim, etc.) skip this shortcut
34    /// and call [`Self::builder`]`.tenant_resolver(...)` directly.
35    #[must_use]
36    pub fn single_tenant(id: impl Into<String>) -> OpsRuntimeBuilder {
37        OpsRuntimeBuilder::default().tenant_resolver(Arc::new(SingleTenant(TenantId(id.into()))))
38    }
39}
40
41/// Builder accumulator.
42pub struct OpsRuntimeBuilder {
43    tenant_resolver: Option<Arc<dyn TenantResolver>>,
44    redactor: SharedRedactor,
45    clock: SharedClock,
46    agent_id: Option<AgentId>,
47    #[cfg(feature = "governor")]
48    governor: Option<Arc<dyn crate::governor::Governor>>,
49    #[cfg(feature = "governor")]
50    governor_provider: Option<ProviderId>,
51    #[cfg(feature = "escalation")]
52    escalation: Option<Arc<dyn crate::escalation::Escalation>>,
53    #[cfg(feature = "gates")]
54    gates: Vec<Arc<dyn crate::gates::Gate>>,
55    #[cfg(feature = "gates")]
56    approval_timeout: Duration,
57    #[cfg(feature = "gates")]
58    approval_timeout_policy: crate::gates::ApprovalTimeoutPolicy,
59}
60
61// Hand-written because DefaultRedactor and SystemClock construct non-trivial
62// Arc values that cannot be expressed with #[derive(Default)].
63impl Default for OpsRuntimeBuilder {
64    fn default() -> Self {
65        Self {
66            tenant_resolver: None,
67            redactor: Arc::new(DefaultRedactor::new()),
68            clock: Arc::new(SystemClock::default()),
69            agent_id: None,
70            #[cfg(feature = "governor")]
71            governor: None,
72            #[cfg(feature = "governor")]
73            governor_provider: None,
74            #[cfg(feature = "escalation")]
75            escalation: None,
76            #[cfg(feature = "gates")]
77            gates: Vec::new(),
78            #[cfg(feature = "gates")]
79            approval_timeout: DEFAULT_APPROVAL_TIMEOUT,
80            #[cfg(feature = "gates")]
81            approval_timeout_policy: crate::gates::ApprovalTimeoutPolicy::default(),
82        }
83    }
84}
85
86impl OpsRuntimeBuilder {
87    /// Set the tenant resolver. Required before calling [`spawn`](Self::spawn).
88    #[must_use]
89    pub fn tenant_resolver(mut self, r: Arc<dyn TenantResolver>) -> Self {
90        self.tenant_resolver = Some(r);
91        self
92    }
93
94    /// Override the default redactor.
95    #[must_use]
96    pub fn redactor(mut self, r: SharedRedactor) -> Self {
97        self.redactor = r;
98        self
99    }
100
101    /// Override the default clock.
102    #[must_use]
103    pub fn clock(mut self, c: SharedClock) -> Self {
104        self.clock = c;
105        self
106    }
107
108    /// Override the agent identity recorded in
109    /// [`crate::OpsEvent::SupervisorStateChange`]. Defaults to the inner
110    /// agent's [`klieo_core::Agent::name`] when not supplied.
111    #[must_use]
112    pub fn agent_id(mut self, id: AgentId) -> Self {
113        self.agent_id = Some(id);
114        self
115    }
116
117    /// Register a [`crate::governor::Governor`] that rate-limits LLM calls made by the
118    /// wrapped agent.
119    #[cfg(feature = "governor")]
120    #[must_use]
121    pub fn governor(mut self, g: Arc<dyn crate::governor::Governor>) -> Self {
122        self.governor = Some(g);
123        self
124    }
125
126    /// Set the provider tag recorded in [`crate::OpsEvent::GovernorDenial`] events.
127    /// Defaults to `"default"` when not supplied.
128    #[cfg(feature = "governor")]
129    #[must_use]
130    pub fn governor_provider(mut self, p: ProviderId) -> Self {
131        self.governor_provider = Some(p);
132        self
133    }
134
135    /// Register an `Escalation` impl. Required when any registered gate
136    /// may emit `RequireApproval`.
137    #[cfg(feature = "escalation")]
138    #[must_use]
139    pub fn escalation(mut self, e: Arc<dyn crate::escalation::Escalation>) -> Self {
140        self.escalation = Some(e);
141        self
142    }
143
144    /// Register a [`crate::gates::Gate`] that guards every tool invocation made by the
145    /// wrapped agent.
146    #[cfg(feature = "gates")]
147    #[must_use]
148    pub fn gate(mut self, gate: Arc<dyn crate::gates::Gate>) -> Self {
149        self.gates.push(gate);
150        self
151    }
152
153    /// Override the approval timeout passed to [`crate::runtime::gated_invoker::GatedToolInvoker`].
154    ///
155    /// How long `GatedToolInvoker` waits for a gate's `wait_for_approval`
156    /// to resolve before returning [`klieo_core::ToolError::Permanent`]. Defaults to
157    /// 600 seconds (10 minutes) per spec § 2.3.
158    #[cfg(feature = "gates")]
159    #[must_use]
160    pub fn approval_timeout(mut self, timeout: Duration) -> Self {
161        self.approval_timeout = timeout;
162        self
163    }
164
165    /// Override the policy applied when `wait_for_approval` times out.
166    ///
167    /// Defaults to `RequeueAndEscalate { max_requeues: 3 }` per spec § 2.3.
168    /// Use `ApprovalTimeoutPolicy::Deny` to restore the pre-v0.4 behaviour
169    /// (terminate immediately on the first timeout).
170    #[cfg(feature = "gates")]
171    #[must_use]
172    pub fn approval_timeout_policy(mut self, policy: crate::gates::ApprovalTimeoutPolicy) -> Self {
173        self.approval_timeout_policy = policy;
174        self
175    }
176
177    /// Validate configuration and wrap the agent.
178    ///
179    /// Returns [`BuildError::MissingDependency`] when a required dependency
180    /// is absent:
181    /// - `tenant_resolver` was not supplied.
182    /// - Any registered gate has `may_require_approval == true` but no
183    ///   `Escalation` was configured.
184    pub fn spawn<A>(self, agent: A) -> Result<SupervisedAgent<A>, BuildError>
185    where
186        A: Agent,
187    {
188        #[cfg(all(feature = "escalation", feature = "gates"))]
189        if self.escalation.is_none() && self.gates.iter().any(|g| g.may_require_approval()) {
190            return Err(BuildError::MissingDependency {
191                needed_by: "Gate (may_require_approval == true)".into(),
192                missing: "escalation".into(),
193            });
194        }
195
196        let tenant_resolver =
197            self.tenant_resolver
198                .ok_or_else(|| BuildError::MissingDependency {
199                    needed_by: "OpsRuntime".into(),
200                    missing: "tenant_resolver".into(),
201                })?;
202
203        let agent_id = self
204            .agent_id
205            .unwrap_or_else(|| AgentId(agent.name().to_string()));
206
207        // redactor and clock are load-bearing once telemetry / redaction hooks land.
208        let _ = (&self.redactor, &self.clock);
209
210        Ok(SupervisedAgent {
211            inner: agent,
212            tenant_resolver,
213            agent_id,
214            #[cfg(feature = "governor")]
215            governor: self.governor,
216            #[cfg(feature = "governor")]
217            governor_provider: self
218                .governor_provider
219                .unwrap_or_else(|| ProviderId("default".into())),
220            #[cfg(feature = "gates")]
221            gates: self.gates,
222            #[cfg(feature = "gates")]
223            approval_timeout: self.approval_timeout,
224            #[cfg(feature = "gates")]
225            approval_timeout_policy: self.approval_timeout_policy,
226        })
227    }
228}