agent_base/engine/
middleware.rs1use std::sync::Arc;
2
3use async_trait::async_trait;
4use serde_json::Value;
5use tokio::sync::broadcast;
6
7use crate::types::{AgentResult, AgentEvent, ChatMessage, SessionId};
8
9#[derive(Clone)]
10pub struct UserMessageCtx {
11 pub session_id: SessionId,
12 pub user_input: String,
13 pub event_bus: broadcast::Sender<AgentEvent>,
14}
15
16#[derive(Clone)]
17pub struct PreLlmCtx {
18 pub session_id: SessionId,
19 pub messages: Vec<ChatMessage>,
20 pub tools: Vec<Value>,
21 pub event_bus: broadcast::Sender<AgentEvent>,
22}
23
24#[derive(Clone)]
25pub struct PostLlmCtx {
26 pub session_id: SessionId,
27 pub full_text: String,
28 pub is_tool_call: bool,
29 pub tool_calls: Vec<(String, String, String)>,
30 pub event_bus: broadcast::Sender<AgentEvent>,
31 pub available_tools: Vec<String>,
32 pub turn_count: u32,
33 pub total_tool_calls: usize,
34 pub skip_push: bool,
35 pub follow_up_message: Option<String>,
36}
37
38#[async_trait]
39pub trait Middleware: Send + Sync {
40 async fn on_user_message(&self, _ctx: &mut UserMessageCtx) -> AgentResult<()> {
41 Ok(())
42 }
43
44 async fn on_pre_llm(&self, _ctx: &mut PreLlmCtx) -> AgentResult<()> {
45 Ok(())
46 }
47
48 async fn on_post_llm(&self, _ctx: &mut PostLlmCtx) -> AgentResult<()> {
49 Ok(())
50 }
51}
52
53pub(crate) type MiddlewareRef = Arc<dyn Middleware>;
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use crate::types::SessionId;
59
60 #[test]
61 fn test_post_llm_ctx_new_fields_defaults() {
62 let (tx, _rx) = broadcast::channel(1);
63 let ctx = PostLlmCtx {
64 session_id: SessionId { id: 1, external_id: None },
65 full_text: "test".to_string(),
66 is_tool_call: false,
67 tool_calls: vec![],
68 event_bus: tx,
69 available_tools: vec![],
70 turn_count: 0,
71 total_tool_calls: 0,
72 skip_push: false,
73 follow_up_message: None,
74 };
75 assert!(ctx.available_tools.is_empty());
76 assert_eq!(ctx.turn_count, 0);
77 assert_eq!(ctx.total_tool_calls, 0);
78 assert!(!ctx.skip_push);
79 assert!(ctx.follow_up_message.is_none());
80 }
81
82 #[test]
83 fn test_post_llm_ctx_skip_push_follow_up_set() {
84 let (tx, _rx) = broadcast::channel(1);
85 let ctx = PostLlmCtx {
86 session_id: SessionId { id: 2, external_id: None },
87 full_text: "I will execute...".to_string(),
88 is_tool_call: false,
89 tool_calls: vec![],
90 event_bus: tx,
91 available_tools: vec!["echo".to_string()],
92 turn_count: 1,
93 total_tool_calls: 0,
94 skip_push: true,
95 follow_up_message: Some("Please call tools now.".to_string()),
96 };
97 assert!(ctx.skip_push);
98 assert_eq!(ctx.follow_up_message, Some("Please call tools now.".to_string()));
99 assert_eq!(ctx.available_tools, vec!["echo".to_string()]);
100 assert_eq!(ctx.total_tool_calls, 0);
101 }
102
103 #[test]
104 fn test_post_llm_ctx_clone_preserves_new_fields() {
105 let (tx, _rx) = broadcast::channel(1);
106 let ctx = PostLlmCtx {
107 session_id: SessionId { id: 3, external_id: None },
108 full_text: "hello".to_string(),
109 is_tool_call: false,
110 tool_calls: vec![],
111 event_bus: tx,
112 available_tools: vec!["add".to_string(), "subtract".to_string()],
113 turn_count: 5,
114 total_tool_calls: 3,
115 skip_push: true,
116 follow_up_message: Some("nudge".to_string()),
117 };
118 let cloned = ctx.clone();
119 assert_eq!(cloned.available_tools, vec!["add", "subtract"]);
120 assert_eq!(cloned.turn_count, 5);
121 assert_eq!(cloned.total_tool_calls, 3);
122 assert!(cloned.skip_push);
123 assert_eq!(cloned.follow_up_message, Some("nudge".to_string()));
124 }
125}