1use serde_json::Value;
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::Arc;
7
8pub mod bridge;
10pub mod builtins;
11pub mod hook;
14pub mod observability_adapters;
19pub mod plugin;
20pub use bridge::build_hooks;
21pub use builtins::{
22 ContentFilterMiddleware, LoggingMiddleware, RateLimitMiddleware, TokenBudgetMiddleware,
23};
24pub use hook::HookMiddleware;
25pub use plugin::{PluginLoader, PluginManifest};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum MiddlewarePhase {
30 BeforeLlm,
32 AfterLlm,
34 BeforeTool,
36 AfterTool,
38 BeforeRun,
40 AfterRun,
42}
43
44#[derive(Clone)]
46pub enum MiddlewareData {
47 BeforeLlm {
49 messages: Vec<oxicode_ai::Message>,
51 model_id: String,
53 },
54 AfterLlm {
56 response_text: String,
58 tokens_used: Option<crate::observability::TokenUsage>,
60 },
61 BeforeTool {
63 tool_name: String,
65 params: Value,
67 },
68 AfterTool {
70 tool_name: String,
72 params: Value,
74 result: String,
76 },
77 BeforeRun {
79 prompt: String,
81 },
82 AfterRun {
84 response: String,
86 success: bool,
88 duration_ms: u64,
90 },
91}
92
93pub struct MiddlewareContext {
95 pub phase: MiddlewarePhase,
97 pub agent_id: String,
99 pub trace_id: Option<crate::observability::TraceId>,
101 pub data: MiddlewareData,
103}
104
105impl MiddlewareContext {
106 pub fn new(phase: MiddlewarePhase, agent_id: &str, data: MiddlewareData) -> Self {
108 Self {
109 phase,
110 agent_id: agent_id.to_string(),
111 trace_id: None,
112 data,
113 }
114 }
115
116 pub fn with_trace(
118 phase: MiddlewarePhase,
119 agent_id: &str,
120 trace_id: crate::observability::TraceId,
121 data: MiddlewareData,
122 ) -> Self {
123 Self {
124 phase,
125 agent_id: agent_id.to_string(),
126 trace_id: Some(trace_id),
127 data,
128 }
129 }
130
131 pub fn tool_name(&self) -> Option<&str> {
133 match &self.data {
134 MiddlewareData::BeforeTool { tool_name, .. } => Some(tool_name),
135 MiddlewareData::AfterTool { tool_name, .. } => Some(tool_name),
136 _ => None,
137 }
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum MiddlewareAction {
144 Continue,
146 Block,
148 Terminate,
150}
151
152#[derive(Clone)]
154pub struct MiddlewareResult {
155 pub action: MiddlewareAction,
157 pub modified_data: Option<MiddlewareData>,
159 pub reason: Option<String>,
161}
162
163impl std::fmt::Debug for MiddlewareResult {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 f.debug_struct("MiddlewareResult")
166 .field("action", &self.action)
167 .field("has_modified_data", &self.modified_data.is_some())
168 .field("reason", &self.reason)
169 .finish()
170 }
171}
172
173impl MiddlewareResult {
174 pub fn pass() -> Self {
176 Self {
177 action: MiddlewareAction::Continue,
178 modified_data: None,
179 reason: None,
180 }
181 }
182 pub fn modify(data: MiddlewareData) -> Self {
184 Self {
185 action: MiddlewareAction::Continue,
186 modified_data: Some(data),
187 reason: None,
188 }
189 }
190 pub fn block(reason: impl Into<String>) -> Self {
192 Self {
193 action: MiddlewareAction::Block,
194 modified_data: None,
195 reason: Some(reason.into()),
196 }
197 }
198 pub fn terminate(reason: impl Into<String>) -> Self {
200 Self {
201 action: MiddlewareAction::Terminate,
202 modified_data: None,
203 reason: Some(reason.into()),
204 }
205 }
206 pub fn is_continue(&self) -> bool {
208 self.action == MiddlewareAction::Continue
209 }
210 pub fn is_block(&self) -> bool {
212 self.action == MiddlewareAction::Block
213 }
214 pub fn is_terminate(&self) -> bool {
216 self.action == MiddlewareAction::Terminate
217 }
218}
219
220pub trait Middleware: Send + Sync {
222 fn name(&self) -> &str;
224 fn phases(&self) -> Vec<MiddlewarePhase>;
226 fn handle<'a>(
228 &'a self,
229 ctx: &'a MiddlewareContext,
230 ) -> Pin<Box<dyn Future<Output = MiddlewareResult> + Send + 'a>>;
231}
232
233#[derive(Default)]
235pub struct MiddlewarePipeline {
236 middlewares: Vec<Arc<dyn Middleware>>,
237}
238
239impl MiddlewarePipeline {
240 pub fn new() -> Self {
242 Self {
243 middlewares: Vec::new(),
244 }
245 }
246 pub fn push<M: Middleware + 'static>(mut self, mw: M) -> Self {
248 self.middlewares.push(Arc::new(mw));
249 self
250 }
251 pub fn add_arc(mut self, mw: Arc<dyn Middleware>) -> Self {
253 self.middlewares.push(mw);
254 self
255 }
256 pub async fn execute(&self, ctx: &MiddlewareContext) -> MiddlewareResult {
258 for mw in &self.middlewares {
259 if !mw.phases().contains(&ctx.phase) {
260 continue;
261 }
262 let result = mw.handle(ctx).await;
263 if !result.is_continue() {
264 return result;
265 }
266 }
267 MiddlewareResult::pass()
268 }
269 pub fn names(&self) -> Vec<&str> {
271 self.middlewares.iter().map(|m| m.name()).collect()
272 }
273 pub fn is_empty(&self) -> bool {
275 self.middlewares.is_empty()
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 struct TestMw;
284 impl Middleware for TestMw {
285 fn name(&self) -> &str {
286 "test"
287 }
288 fn phases(&self) -> Vec<MiddlewarePhase> {
289 vec![MiddlewarePhase::BeforeTool]
290 }
291 fn handle<'a>(
292 &'a self,
293 _ctx: &'a MiddlewareContext,
294 ) -> Pin<Box<dyn Future<Output = MiddlewareResult> + Send + 'a>> {
295 Box::pin(async { MiddlewareResult::pass() })
296 }
297 }
298
299 #[tokio::test]
300 async fn test_pipeline() {
301 let p = MiddlewarePipeline::new().push(TestMw);
302 let ctx = MiddlewareContext::new(
303 MiddlewarePhase::BeforeTool,
304 "a1",
305 MiddlewareData::BeforeTool {
306 tool_name: "read".into(),
307 params: serde_json::json!({}),
308 },
309 );
310 assert!(p.execute(&ctx).await.is_continue());
311 }
312
313 #[tokio::test]
314 async fn test_pipeline_skips_unrelated_phases() {
315 struct BeforeToolOnly;
316 impl Middleware for BeforeToolOnly {
317 fn name(&self) -> &str {
318 "before_only"
319 }
320 fn phases(&self) -> Vec<MiddlewarePhase> {
321 vec![MiddlewarePhase::BeforeTool]
322 }
323 fn handle<'a>(
324 &'a self,
325 _ctx: &'a MiddlewareContext,
326 ) -> Pin<Box<dyn Future<Output = MiddlewareResult> + Send + 'a>> {
327 Box::pin(async { MiddlewareResult::block("should not run") })
328 }
329 }
330 let p = MiddlewarePipeline::new().push(BeforeToolOnly);
331 let ctx = MiddlewareContext::new(
332 MiddlewarePhase::AfterLlm,
333 "a1",
334 MiddlewareData::AfterLlm {
335 response_text: "hello".into(),
336 tokens_used: None,
337 },
338 );
339 assert!(p.execute(&ctx).await.is_continue());
341 }
342
343 #[test]
344 fn test_middleware_result_modify() {
345 let data = MiddlewareData::BeforeTool {
346 tool_name: "read".into(),
347 params: serde_json::json!({"path": "/tmp"}),
348 };
349 let result = MiddlewareResult::modify(data);
350 assert!(result.is_continue());
351 assert!(result.modified_data.is_some());
352 }
353
354 #[test]
355 fn test_middleware_context_with_trace() {
356 use crate::observability::TraceId;
357 let trace_id = TraceId::new();
358 let ctx = MiddlewareContext::with_trace(
359 MiddlewarePhase::BeforeTool,
360 "a1",
361 trace_id,
362 MiddlewareData::BeforeTool {
363 tool_name: "read".into(),
364 params: serde_json::json!({}),
365 },
366 );
367 assert_eq!(ctx.trace_id, Some(trace_id));
368 assert_eq!(ctx.agent_id, "a1");
369 }
370}