1use std::sync::Arc;
12use std::time::{Duration, Instant};
13
14use async_trait::async_trait;
15use atomr_agents_core::{AgentError, AgentId, Result, Value};
16use atomr_infer_core::batch::ExecuteBatch;
17use parking_lot::Mutex;
18
19use crate::inference::TurnResult;
20
21#[async_trait]
22pub trait AgentMiddleware: Send + Sync + 'static {
23 async fn before_agent(&self, _agent_id: &AgentId, _user: &str) -> Result<()> {
24 Ok(())
25 }
26 async fn before_model_call(&self, _batch: &mut ExecuteBatch) -> Result<()> {
27 Ok(())
28 }
29 async fn after_model_call(&self, _result: &mut TurnResult) -> Result<()> {
30 Ok(())
31 }
32 async fn before_tool_call(&self, _name: &str, _args: &mut Value) -> Result<()> {
33 Ok(())
34 }
35 async fn after_tool_call(&self, _name: &str, _result: &mut Result<Value>) -> Result<()> {
36 Ok(())
37 }
38 async fn after_agent(&self, _result: &mut TurnResult) -> Result<()> {
39 Ok(())
40 }
41 async fn dynamic_prompt(&self, _agent_id: &AgentId, _user: &str) -> Result<Option<String>> {
43 Ok(None)
44 }
45}
46
47#[derive(Default, Clone)]
49pub struct MiddlewareStack {
50 inner: Vec<Arc<dyn AgentMiddleware>>,
51}
52
53impl MiddlewareStack {
54 pub fn new() -> Self {
55 Self::default()
56 }
57
58 pub fn push(mut self, m: Arc<dyn AgentMiddleware>) -> Self {
59 self.inner.push(m);
60 self
61 }
62
63 pub fn iter(&self) -> impl Iterator<Item = &Arc<dyn AgentMiddleware>> {
64 self.inner.iter()
65 }
66
67 pub fn iter_rev(&self) -> impl Iterator<Item = &Arc<dyn AgentMiddleware>> {
68 self.inner.iter().rev()
69 }
70
71 pub async fn run_before_agent(&self, agent_id: &AgentId, user: &str) -> Result<()> {
72 for m in self.iter() {
73 m.before_agent(agent_id, user).await?;
74 }
75 Ok(())
76 }
77
78 pub async fn run_before_model_call(&self, batch: &mut ExecuteBatch) -> Result<()> {
79 for m in self.iter() {
80 m.before_model_call(batch).await?;
81 }
82 Ok(())
83 }
84
85 pub async fn run_after_model_call(&self, result: &mut TurnResult) -> Result<()> {
86 for m in self.iter_rev() {
87 m.after_model_call(result).await?;
88 }
89 Ok(())
90 }
91
92 pub async fn run_before_tool_call(&self, name: &str, args: &mut Value) -> Result<()> {
93 for m in self.iter() {
94 m.before_tool_call(name, args).await?;
95 }
96 Ok(())
97 }
98
99 pub async fn run_after_tool_call(&self, name: &str, result: &mut Result<Value>) -> Result<()> {
100 for m in self.iter_rev() {
101 m.after_tool_call(name, result).await?;
102 }
103 Ok(())
104 }
105
106 pub async fn run_after_agent(&self, result: &mut TurnResult) -> Result<()> {
107 for m in self.iter_rev() {
108 m.after_agent(result).await?;
109 }
110 Ok(())
111 }
112
113 pub async fn run_dynamic_prompt(&self, agent_id: &AgentId, user: &str) -> Result<Option<String>> {
114 let mut out: Option<String> = None;
116 for m in self.iter() {
117 if let Some(s) = m.dynamic_prompt(agent_id, user).await? {
118 out = Some(s);
119 }
120 }
121 Ok(out)
122 }
123}
124
125#[derive(Default, Clone)]
130pub struct LoggingMiddleware {
131 pub log: Arc<Mutex<Vec<String>>>,
132}
133
134impl LoggingMiddleware {
135 pub fn new() -> Self {
136 Self::default()
137 }
138
139 pub fn lines(&self) -> Vec<String> {
140 self.log.lock().clone()
141 }
142}
143
144#[async_trait]
145impl AgentMiddleware for LoggingMiddleware {
146 async fn before_agent(&self, agent_id: &AgentId, user: &str) -> Result<()> {
147 self.log
148 .lock()
149 .push(format!("before_agent {} '{}'", agent_id.as_str(), user));
150 Ok(())
151 }
152 async fn before_model_call(&self, batch: &mut ExecuteBatch) -> Result<()> {
153 self.log
154 .lock()
155 .push(format!("before_model_call model={}", batch.model));
156 Ok(())
157 }
158 async fn after_model_call(&self, result: &mut TurnResult) -> Result<()> {
159 self.log.lock().push(format!(
160 "after_model_call out_tokens={}",
161 result.usage.output_tokens
162 ));
163 Ok(())
164 }
165 async fn before_tool_call(&self, name: &str, _args: &mut Value) -> Result<()> {
166 self.log.lock().push(format!("before_tool_call {name}"));
167 Ok(())
168 }
169 async fn after_tool_call(&self, name: &str, result: &mut Result<Value>) -> Result<()> {
170 let ok = result.is_ok();
171 self.log.lock().push(format!("after_tool_call {name} ok={ok}"));
172 Ok(())
173 }
174 async fn after_agent(&self, _r: &mut TurnResult) -> Result<()> {
175 self.log.lock().push("after_agent".into());
176 Ok(())
177 }
178}
179
180pub struct RateLimitMiddleware {
185 capacity: u32,
186 refill_per_sec: u32,
187 state: Mutex<BucketState>,
188}
189
190struct BucketState {
191 tokens: f32,
192 last: Instant,
193}
194
195impl RateLimitMiddleware {
196 pub fn new(capacity: u32, refill_per_sec: u32) -> Self {
197 Self {
198 capacity,
199 refill_per_sec,
200 state: Mutex::new(BucketState {
201 tokens: capacity as f32,
202 last: Instant::now(),
203 }),
204 }
205 }
206
207 fn try_take(&self) -> bool {
208 let mut s = self.state.lock();
209 let now = Instant::now();
210 let elapsed = now.duration_since(s.last).as_secs_f32();
211 s.tokens = (s.tokens + elapsed * self.refill_per_sec as f32).min(self.capacity as f32);
212 s.last = now;
213 if s.tokens >= 1.0 {
214 s.tokens -= 1.0;
215 true
216 } else {
217 false
218 }
219 }
220}
221
222#[async_trait]
223impl AgentMiddleware for RateLimitMiddleware {
224 async fn before_model_call(&self, _batch: &mut ExecuteBatch) -> Result<()> {
225 let mut waited = Duration::ZERO;
226 while !self.try_take() {
227 let backoff = Duration::from_millis(50);
228 tokio::time::sleep(backoff).await;
229 waited += backoff;
230 if waited > Duration::from_secs(10) {
231 return Err(AgentError::Inference("rate-limit: gave up after 10s".into()));
232 }
233 }
234 Ok(())
235 }
236}
237
238pub struct RedactionMiddleware {
244 pub patterns: Vec<String>,
245 pub replacement: String,
246}
247
248impl RedactionMiddleware {
249 pub fn new(patterns: Vec<String>, replacement: impl Into<String>) -> Self {
250 Self {
251 patterns,
252 replacement: replacement.into(),
253 }
254 }
255}
256
257#[async_trait]
258impl AgentMiddleware for RedactionMiddleware {
259 async fn before_model_call(&self, batch: &mut ExecuteBatch) -> Result<()> {
260 for msg in &mut batch.messages {
261 if let atomr_infer_core::batch::MessageContent::Text(t) = &mut msg.content {
262 for p in &self.patterns {
263 *t = t.replace(p, &self.replacement);
264 }
265 }
266 }
267 Ok(())
268 }
269}
270
271pub struct ToolErrorRecoveryMiddleware;
277
278#[async_trait]
279impl AgentMiddleware for ToolErrorRecoveryMiddleware {
280 async fn after_tool_call(&self, name: &str, result: &mut Result<Value>) -> Result<()> {
281 if let Err(e) = result {
282 let payload = serde_json::json!({ "tool_error": true, "tool": name, "message": e.to_string() });
283 *result = Ok(payload);
284 }
285 Ok(())
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use atomr_infer_core::batch::{ExecuteBatch, MessageContent, SamplingParams};
293
294 fn batch(model: &str, user_text: &str) -> ExecuteBatch {
295 ExecuteBatch {
296 request_id: "r".into(),
297 model: model.into(),
298 messages: vec![atomr_infer_core::batch::Message {
299 role: atomr_infer_core::batch::Role::User,
300 content: MessageContent::Text(user_text.into()),
301 }],
302 sampling: SamplingParams::default(),
303 stream: false,
304 estimated_tokens: 1,
305 }
306 }
307
308 #[tokio::test]
309 async fn logging_records_each_phase() {
310 let m: Arc<dyn AgentMiddleware> = Arc::new(LoggingMiddleware::new());
311 let stack = MiddlewareStack::new().push(m.clone());
312 stack.run_before_agent(&AgentId::from("a"), "hi").await.unwrap();
313 let mut b = batch("mock", "hi");
314 stack.run_before_model_call(&mut b).await.unwrap();
315 let m_dc: &LoggingMiddleware = unsafe { &*(Arc::as_ptr(&m) as *const LoggingMiddleware) };
316 assert!(m_dc.lines().iter().any(|l| l.starts_with("before_agent")));
317 assert!(m_dc.lines().iter().any(|l| l.starts_with("before_model_call")));
318 }
319
320 #[tokio::test]
321 async fn redaction_strips_patterns() {
322 let stack = MiddlewareStack::new().push(Arc::new(RedactionMiddleware::new(
323 vec!["secret".into()],
324 "[redacted]",
325 )));
326 let mut b = batch("mock", "the secret is out");
327 stack.run_before_model_call(&mut b).await.unwrap();
328 let MessageContent::Text(t) = &b.messages[0].content else {
329 panic!("expected text");
330 };
331 assert_eq!(t, "the [redacted] is out");
332 }
333
334 #[tokio::test]
335 async fn tool_error_recovery_converts_err_to_payload() {
336 let stack = MiddlewareStack::new().push(Arc::new(ToolErrorRecoveryMiddleware));
337 let mut r: Result<Value> = Err(AgentError::Tool("boom".into()));
338 stack.run_after_tool_call("calc", &mut r).await.unwrap();
339 let v = r.unwrap();
340 assert_eq!(v["tool_error"], true);
341 assert_eq!(v["tool"], "calc");
342 }
343
344 #[tokio::test]
345 async fn rate_limit_allows_burst_then_blocks() {
346 let m: Arc<dyn AgentMiddleware> = Arc::new(RateLimitMiddleware::new(2, 1));
347 let stack = MiddlewareStack::new().push(m);
348 let mut b = batch("m", "hi");
349 let t0 = Instant::now();
351 stack.run_before_model_call(&mut b).await.unwrap();
352 stack.run_before_model_call(&mut b).await.unwrap();
353 let warm = t0.elapsed();
354 assert!(warm < Duration::from_millis(100));
355 let t1 = Instant::now();
357 stack.run_before_model_call(&mut b).await.unwrap();
358 let cold = t1.elapsed();
359 assert!(cold >= Duration::from_millis(40));
360 }
361}