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}