use crate::{
providers::LLMRequest,
shared::{ContentBlock, Message, ToolCall},
state::AgentState,
};
use async_trait::async_trait;
use std::sync::Arc;
pub struct MiddlewareCtx<'a> {
pub session_id: &'a str,
pub state: &'a AgentState,
}
pub enum ToolDecision {
Proceed,
ShortCircuit {
output: Vec<ContentBlock>,
is_error: bool,
},
}
#[async_trait]
pub trait Middleware: Send + Sync {
fn name(&self) -> &str;
async fn before_model(
&self,
_ctx: &MiddlewareCtx<'_>,
_req: &mut LLMRequest,
) -> Result<(), String> {
Ok(())
}
async fn after_model(
&self,
_ctx: &MiddlewareCtx<'_>,
_msg: &mut Message,
) -> Result<(), String> {
Ok(())
}
async fn before_tool(
&self,
_ctx: &MiddlewareCtx<'_>,
_call: &ToolCall,
) -> Result<ToolDecision, String> {
Ok(ToolDecision::Proceed)
}
async fn after_tool(
&self,
_ctx: &MiddlewareCtx<'_>,
_call: &ToolCall,
_result: &mut ContentBlock,
) -> Result<(), String> {
Ok(())
}
}
#[derive(Clone, Default)]
pub struct MiddlewareChain {
middlewares: Vec<Arc<dyn Middleware>>,
}
impl MiddlewareChain {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, middleware: Arc<dyn Middleware>) {
self.middlewares.push(middleware);
}
pub fn with(mut self, middleware: Arc<dyn Middleware>) -> Self {
self.middlewares.push(middleware);
self
}
pub fn is_empty(&self) -> bool {
self.middlewares.is_empty()
}
pub async fn before_model(
&self,
ctx: &MiddlewareCtx<'_>,
req: &mut LLMRequest,
) -> Result<(), String> {
for m in &self.middlewares {
m.before_model(ctx, req)
.await
.map_err(|e| format!("[{}] {e}", m.name()))?;
}
Ok(())
}
pub async fn after_model(
&self,
ctx: &MiddlewareCtx<'_>,
msg: &mut Message,
) -> Result<(), String> {
for m in self.middlewares.iter().rev() {
m.after_model(ctx, msg)
.await
.map_err(|e| format!("[{}] {e}", m.name()))?;
}
Ok(())
}
pub async fn before_tool(
&self,
ctx: &MiddlewareCtx<'_>,
call: &ToolCall,
) -> Result<ToolDecision, String> {
for m in &self.middlewares {
match m
.before_tool(ctx, call)
.await
.map_err(|e| format!("[{}] {e}", m.name()))?
{
ToolDecision::Proceed => {}
short_circuit => return Ok(short_circuit),
}
}
Ok(ToolDecision::Proceed)
}
pub async fn after_tool(
&self,
ctx: &MiddlewareCtx<'_>,
call: &ToolCall,
result: &mut ContentBlock,
) -> Result<(), String> {
for m in self.middlewares.iter().rev() {
m.after_tool(ctx, call, result)
.await
.map_err(|e| format!("[{}] {e}", m.name()))?;
}
Ok(())
}
}