1use std::sync::Arc;
2
3use async_trait::async_trait;
4use serde_json::Value;
5
6use crate::types::{AgentResult, ChatMessage, FinishReason, SessionId, UserEvent};
7
8#[derive(Clone)]
9pub struct UserMessageCtx {
10 pub session_id: SessionId,
11 pub user_input: String,
12}
13
14pub struct PreLlmCtx {
15 pub session_id: SessionId,
16 pub messages: Vec<ChatMessage>,
17 pub tools: Vec<Value>,
18 pub emit_fn: Option<Box<dyn Fn(UserEvent) + Send + Sync>>,
25 pub turn_count: u32,
27 pub max_turns: u32,
29}
30
31impl PreLlmCtx {
32 pub fn emit(&self, event: UserEvent) {
41 if let Some(ref f) = self.emit_fn
42 && let Err(e) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
43 f(event);
44 }))
45 {
46 tracing::warn!("emit failed: {:?}", e);
47 }
48 }
49}
50
51#[derive(Clone)]
52pub struct PostLlmCtx {
53 pub session_id: SessionId,
54 pub full_text: String,
55 pub is_tool_call: bool,
56 pub tool_calls: Vec<(String, String, String)>,
57 pub available_tools: Vec<String>,
58 pub turn_count: u32,
59 pub total_tool_calls: usize,
60 pub nudge_count: usize,
63 pub turn_tool_calls: usize,
66 pub skip_push: bool,
67 pub follow_up_message: Option<String>,
68 pub finish_reason: FinishReason,
72}
73
74#[async_trait]
75pub trait Middleware: Send + Sync {
76 async fn on_user_message(&self, _ctx: &mut UserMessageCtx) -> AgentResult<()> {
77 Ok(())
78 }
79
80 async fn on_pre_llm(&self, _ctx: &mut PreLlmCtx) -> AgentResult<()> {
81 Ok(())
82 }
83
84 async fn on_post_llm(&self, _ctx: &mut PostLlmCtx) -> AgentResult<()> {
85 Ok(())
86 }
87}
88
89pub(crate) type MiddlewareRef = Arc<dyn Middleware>;
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::types::SessionId;
95
96 #[test]
97 fn test_post_llm_ctx_new_fields_defaults() {
98 let ctx = PostLlmCtx {
99 session_id: SessionId {
100 id: 1,
101 external_id: None,
102 },
103 full_text: "test".to_string(),
104 is_tool_call: false,
105 tool_calls: vec![],
106 available_tools: vec![],
107 turn_count: 0,
108 total_tool_calls: 0,
109 nudge_count: 0,
110 turn_tool_calls: 0,
111 skip_push: false,
112 follow_up_message: None,
113 finish_reason: FinishReason::Stop,
114 };
115 assert!(ctx.available_tools.is_empty());
116 assert_eq!(ctx.turn_count, 0);
117 assert_eq!(ctx.total_tool_calls, 0);
118 assert!(!ctx.skip_push);
119 assert!(ctx.follow_up_message.is_none());
120 assert_eq!(ctx.finish_reason, FinishReason::Stop);
121 }
122
123 #[test]
124 fn test_post_llm_ctx_skip_push_follow_up_set() {
125 let ctx = PostLlmCtx {
126 session_id: SessionId {
127 id: 2,
128 external_id: None,
129 },
130 full_text: "I will execute...".to_string(),
131 is_tool_call: false,
132 tool_calls: vec![],
133 available_tools: vec!["echo".to_string()],
134 turn_count: 1,
135 total_tool_calls: 0,
136 nudge_count: 0,
137 turn_tool_calls: 0,
138 skip_push: true,
139 follow_up_message: Some("Please call tools now.".to_string()),
140 finish_reason: FinishReason::Stop,
141 };
142 assert!(ctx.skip_push);
143 assert_eq!(
144 ctx.follow_up_message,
145 Some("Please call tools now.".to_string())
146 );
147 assert_eq!(ctx.available_tools, vec!["echo".to_string()]);
148 assert_eq!(ctx.total_tool_calls, 0);
149 }
150
151 #[test]
152 fn test_post_llm_ctx_clone_preserves_new_fields() {
153 let ctx = PostLlmCtx {
154 session_id: SessionId {
155 id: 3,
156 external_id: None,
157 },
158 full_text: "hello".to_string(),
159 is_tool_call: false,
160 tool_calls: vec![],
161 available_tools: vec!["add".to_string(), "subtract".to_string()],
162 turn_count: 5,
163 total_tool_calls: 3,
164 nudge_count: 0,
165 turn_tool_calls: 2,
166 skip_push: true,
167 follow_up_message: Some("nudge".to_string()),
168 finish_reason: FinishReason::Truncated {
169 reason: Some("max_tokens".into()),
170 },
171 };
172 let cloned = ctx.clone();
173 assert_eq!(cloned.available_tools, vec!["add", "subtract"]);
174 assert_eq!(cloned.turn_count, 5);
175 assert_eq!(cloned.total_tool_calls, 3);
176 assert_eq!(cloned.turn_tool_calls, 2);
177 assert!(cloned.skip_push);
178 assert_eq!(cloned.follow_up_message, Some("nudge".to_string()));
179 assert!(cloned.finish_reason.is_truncated());
180 }
181}