Skip to main content

oxicode_sdk/observability/
decorator.rs

1//! Agent decorator — pluggable transform applied to every agent a
2//! supervisor spawns.
3//!
4//! Built-in [`ObservabilityDecorator`] bundles the four observability
5//! pieces (audit, authorizer, tracer, cost tracker) that previously had
6//! silent no-op setters on [`SupervisorBuilder`](crate::builder::SupervisorBuilder). The decorator
7//! is applied to the [`crate::AgentBuilder`] *before* `.build()` runs,
8//! so the agent's middleware pipeline + event-tap dispatch are wired
9//! exactly as if the caller had set each piece per-agent via
10//! `Oxicode::agent(...).audit_log(...)`.
11//!
12//! See `docs/designs/2026-07-18-stub-completion.md` §4.7.
13
14use std::sync::Arc;
15
16use crate::agent_builder::AgentBuilder;
17use crate::observability::{AuditLog, CostTracker, Tracer};
18use crate::security::Authorizer;
19
20/// Pluggable transform applied to every agent a supervisor spawns.
21///
22/// Implementations receive an [`AgentBuilder`] (already bound to the
23/// supervisor's `Oxicode` instance and seeded with the spawn config) and
24/// return a (possibly modified) builder. The supervisor then calls
25/// `.build()` on the returned builder.
26///
27/// This is the integration point for cross-cutting concerns that
28/// should apply to *every* supervisor-spawned agent — observability,
29/// capability enforcement, middleware injection, etc.
30///
31/// # Example
32///
33/// ```no_run
34/// use std::sync::Arc;
35/// use oxicode_sdk::{
36///     observability::{AgentDecorator, ObservabilityDecorator, AuditLog, Tracer},
37///     OxicodeBuilder,
38/// };
39///
40/// let decorator = ObservabilityDecorator::new()
41///     .with_audit(Arc::new(AuditLog::new(256)))
42///     .with_tracer(Arc::new(Tracer::new()));
43///
44/// let (oxicode, supervisor) = OxicodeBuilder::new()
45///     .with_builtins()
46///     .supervisor()
47///     .with_agent_decorator(Arc::new(decorator))
48///     .build()
49///     .unwrap();
50/// # let _ = (oxicode, supervisor);
51/// ```
52pub trait AgentDecorator: Send + Sync {
53    /// Apply this decorator's transforms to the builder. The
54    /// implementor may chain `audit_log`, `tracer`, `authorizer`,
55    /// `cost_tracker`, `capabilities`, middleware, etc., then return
56    /// the builder for the caller to `.build()`.
57    fn decorate<'a>(&self, builder: AgentBuilder<'a>) -> AgentBuilder<'a>;
58}
59/// Built-in decorator that bundles audit, authorizer, tracer, and cost
60/// tracker observability and applies them to every spawned agent.
61///
62/// Construct with [`ObservabilityDecorator::new`] (all fields
63/// `None`) then attach the pieces you want via the `with_*` builders.
64/// Fields left `None` are simply not applied — no warning, no
65/// silent-drop. This replaces the four deprecated no-op setters on
66/// `SupervisorBuilder` (`with_audit`, `with_authorizer`,
67/// `with_tracer`, `with_cost_tracker`).
68#[derive(Default, Clone)]
69pub struct ObservabilityDecorator {
70    audit: Option<Arc<AuditLog>>,
71    authorizer: Option<Arc<Authorizer>>,
72    tracer: Option<Arc<Tracer>>,
73    cost_tracker: Option<Arc<CostTracker>>,
74}
75
76impl ObservabilityDecorator {
77    /// Create an empty decorator (all observability pieces `None`).
78    pub fn new() -> Self {
79        Self::default()
80    }
81
82    /// Attach an audit log. Replaces any previously-set audit log.
83    pub fn with_audit(mut self, audit: Arc<AuditLog>) -> Self {
84        self.audit = Some(audit);
85        self
86    }
87
88    /// Attach an authorizer. Replaces any previously-set authorizer.
89    pub fn with_authorizer(mut self, authorizer: Arc<Authorizer>) -> Self {
90        self.authorizer = Some(authorizer);
91        self
92    }
93
94    /// Attach a tracer. Replaces any previously-set tracer.
95    pub fn with_tracer(mut self, tracer: Arc<Tracer>) -> Self {
96        self.tracer = Some(tracer);
97        self
98    }
99
100    /// Attach a cost tracker. Replaces any previously-set tracker.
101    pub fn with_cost_tracker(mut self, tracker: Arc<CostTracker>) -> Self {
102        self.cost_tracker = Some(tracker);
103        self
104    }
105
106    /// Borrow the audit log, if set.
107    pub fn audit(&self) -> Option<&Arc<AuditLog>> {
108        self.audit.as_ref()
109    }
110
111    /// Borrow the authorizer, if set.
112    pub fn authorizer(&self) -> Option<&Arc<Authorizer>> {
113        self.authorizer.as_ref()
114    }
115
116    /// Borrow the tracer, if set.
117    pub fn tracer(&self) -> Option<&Arc<Tracer>> {
118        self.tracer.as_ref()
119    }
120
121    /// Borrow the cost tracker, if set.
122    pub fn cost_tracker(&self) -> Option<&Arc<CostTracker>> {
123        self.cost_tracker.as_ref()
124    }
125}
126
127impl AgentDecorator for ObservabilityDecorator {
128    fn decorate<'a>(&self, builder: AgentBuilder<'a>) -> AgentBuilder<'a> {
129        let b = builder;
130        let b = if let Some(a) = self.audit.as_ref() {
131            b.audit_log(a.clone())
132        } else {
133            b
134        };
135        let b = if let Some(a) = self.authorizer.as_ref() {
136            b.authorizer(a.clone())
137        } else {
138            b
139        };
140        let b = if let Some(t) = self.tracer.as_ref() {
141            b.tracer(t.clone())
142        } else {
143            b
144        };
145        if let Some(c) = self.cost_tracker.as_ref() {
146            b.cost_tracker(c.clone())
147        } else {
148            b
149        }
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use crate::observability::{AuditLog, CostTrackerConfig, Tracer};
157    use crate::security::Authorizer;
158    use oxicode_ai::ModelRegistry;
159
160    #[test]
161    fn empty_decorator_is_noop_on_fields() {
162        let d = ObservabilityDecorator::new();
163        assert!(d.audit().is_none());
164        assert!(d.authorizer().is_none());
165        assert!(d.tracer().is_none());
166        assert!(d.cost_tracker().is_none());
167    }
168
169    #[test]
170    fn builders_set_fields() {
171        let audit = Arc::new(AuditLog::new(64));
172        let tracer = Arc::new(Tracer::new());
173        let authorizer = Arc::new(Authorizer::new(audit.clone()));
174        let registry = Arc::new(ModelRegistry::new());
175        let cost = Arc::new(CostTracker::new(registry, CostTrackerConfig::default()));
176
177        let d = ObservabilityDecorator::new()
178            .with_audit(audit.clone())
179            .with_tracer(tracer.clone())
180            .with_authorizer(authorizer.clone())
181            .with_cost_tracker(cost.clone());
182
183        assert!(Arc::ptr_eq(d.audit().unwrap(), &audit));
184        assert!(Arc::ptr_eq(d.tracer().unwrap(), &tracer));
185        assert!(Arc::ptr_eq(d.authorizer().unwrap(), &authorizer));
186        assert!(Arc::ptr_eq(d.cost_tracker().unwrap(), &cost));
187    }
188
189    #[test]
190    fn decorator_implements_send_sync() {
191        fn assert_send_sync<T: Send + Sync>() {}
192        assert_send_sync::<ObservabilityDecorator>();
193        assert_send_sync::<Arc<dyn AgentDecorator>>();
194    }
195}