1use std::sync::Arc;
2use std::time::Duration;
3
4use async_trait::async_trait;
5use serde_json::Value;
6
7use crate::engine::EventBus;
8use crate::tool::{Tool, ToolContext, ToolControlFlow, ToolOutput, ToolPolicy, TruncationInfo};
9use crate::types::{RuntimeEvent, AgentResult};
10
11#[async_trait]
16pub trait ToolExecutionPipeline: Send + Sync {
17 async fn execute(
18 &self,
19 tool: &dyn Tool,
20 args: &Value,
21 ctx: &ToolContext,
22 ) -> AgentResult<ToolOutput>;
23}
24
25#[derive(Clone)]
27pub struct DefaultPipeline {
28 tool_policy: Option<Arc<dyn ToolPolicy>>,
29 tool_timeout_ms: Option<u64>,
30 max_output_chars: Option<usize>,
31}
32
33impl DefaultPipeline {
34 pub fn new(
35 tool_policy: Option<Arc<dyn ToolPolicy>>,
36 tool_timeout_ms: Option<u64>,
37 max_output_chars: Option<usize>,
38 ) -> Self {
39 Self {
40 tool_policy,
41 tool_timeout_ms,
42 max_output_chars,
43 }
44 }
45
46 pub fn policy(&self) -> Option<Arc<dyn ToolPolicy>> {
47 self.tool_policy.clone()
48 }
49}
50
51#[async_trait]
52impl ToolExecutionPipeline for DefaultPipeline {
53 async fn execute(
54 &self,
55 tool: &dyn Tool,
56 args: &Value,
57 ctx: &ToolContext,
58 ) -> AgentResult<ToolOutput> {
59 if let Some(policy) = &self.tool_policy {
61 policy.before_call(tool.name(), args, ctx)?;
62 }
63
64 let result = if let Some(timeout_ms) = self.tool_timeout_ms {
66 match tokio::time::timeout(
67 Duration::from_millis(timeout_ms),
68 tool.call(args, ctx),
69 )
70 .await
71 {
72 Ok(result) => result,
73 Err(_) => {
74 tracing::warn!(
75 tool = tool.name(),
76 timeout_ms = timeout_ms,
77 "tool execution timed out"
78 );
79 return Ok(ToolOutput {
80 summary: "[Tool Timeout]".to_string(),
81 control_flow: ToolControlFlow::Continue,
82 ..Default::default()
83 });
84 }
85 }
86 } else {
87 tool.call(args, ctx).await
88 };
89
90 let mut output = result?;
91
92 if let Some(max_chars) = self.max_output_chars {
94 if output.summary.len() > max_chars {
95 let original_summary_len = output.summary.len();
96 let original_raw_len = output.raw.as_ref().map(|v| v.to_string().len());
97 let suffix = "...(truncated)";
98 let keep = max_chars.saturating_sub(suffix.len());
99 if keep > 0 {
100 let truncate_at = output.summary.floor_char_boundary(keep);
104 output.summary.truncate(truncate_at);
105 output.summary.push_str(suffix);
106 } else {
107 output.summary = suffix[..max_chars].to_string();
108 }
109 output.truncation = Some(TruncationInfo {
110 original_summary_len,
111 original_raw_len,
112 max_allowed_chars: max_chars,
113 });
114 tracing::debug!(
115 tool = tool.name(),
116 original_summary_len = original_summary_len,
117 original_raw_len = original_raw_len,
118 max_allowed_chars = max_chars,
119 "tool output truncated"
120 );
121 }
122 }
123
124 if let Some(policy) = &self.tool_policy {
126 policy.after_call(tool.name(), args, &output, ctx)?;
127 }
128
129 Ok(output)
130 }
131}
132
133pub(crate) struct EventEmittingPipeline<P: ToolExecutionPipeline> {
136 inner: P,
137 event_bus: EventBus,
138}
139
140impl<P: ToolExecutionPipeline> EventEmittingPipeline<P> {
141 pub fn new(inner: P, event_bus: EventBus) -> Self {
142 Self { inner, event_bus }
143 }
144}
145
146#[async_trait]
147impl<P: ToolExecutionPipeline + Send + Sync> ToolExecutionPipeline for EventEmittingPipeline<P> {
148 async fn execute(
149 &self,
150 tool: &dyn Tool,
151 args: &Value,
152 ctx: &ToolContext,
153 ) -> AgentResult<ToolOutput> {
154 self.event_bus.emit(RuntimeEvent::ToolCallStarted {
155 session_id: ctx.session_id.clone(),
156 tool_name: tool.name().to_string(),
157 args_json: args.to_string(),
158 });
159
160 let result = self.inner.execute(tool, args, ctx).await;
161
162 let summary = match &result {
163 Ok(output) => output.summary.clone(),
164 Err(e) => e.to_string(),
165 };
166 self.event_bus.emit(RuntimeEvent::ToolCallFinished {
167 session_id: ctx.session_id.clone(),
168 tool_name: tool.name().to_string(),
169 summary,
170 });
171
172 result
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use serde_json::json;
180 use std::sync::atomic::{AtomicU32, Ordering};
181
182 use crate::tool::ToolRegistry;
183 use crate::types::{AgentError, Language, SessionId};
184
185 use tokio::sync::mpsc;
188
189 fn test_ctx() -> ToolContext {
190 let (tx, _rx) = mpsc::unbounded_channel();
191 ToolContext {
192 session_id: SessionId::new(1),
193 user_event_tx: tx,
194 llm_client: None,
195 session_store: None,
196 language: Language::En,
197 cancel_token: tokio_util::sync::CancellationToken::new(),
198 }
199 }
200
201 struct EchoTool;
202 #[async_trait]
203 impl Tool for EchoTool {
204 fn name(&self) -> &'static str { "echo" }
205 fn definition(&self) -> Value { json!({}) }
206 async fn call(&self, args: &Value, _ctx: &ToolContext) -> AgentResult<ToolOutput> {
207 Ok(ToolOutput {
208 summary: args.get("msg").and_then(|v| v.as_str()).unwrap_or("ok").to_string(),
209 ..Default::default()
210 })
211 }
212 }
213
214 struct SlowTool;
215 #[async_trait]
216 impl Tool for SlowTool {
217 fn name(&self) -> &'static str { "slow" }
218 fn definition(&self) -> Value { json!({}) }
219 async fn call(&self, _args: &Value, _ctx: &ToolContext) -> AgentResult<ToolOutput> {
220 tokio::time::sleep(Duration::from_secs(10)).await;
221 Ok(ToolOutput { summary: "done".to_string(), ..Default::default() })
222 }
223 }
224
225 struct FailingTool;
226 #[async_trait]
227 impl Tool for FailingTool {
228 fn name(&self) -> &'static str { "fail" }
229 fn definition(&self) -> Value { json!({}) }
230 async fn call(&self, _args: &Value, _ctx: &ToolContext) -> AgentResult<ToolOutput> {
231 Err(AgentError::tool_not_found("intentional"))
232 }
233 }
234
235 struct TrackingPolicy {
236 before_count: AtomicU32,
237 after_count: AtomicU32,
238 fail_before: bool,
239 }
240 impl TrackingPolicy {
241 fn new() -> Self {
242 Self { before_count: AtomicU32::new(0), after_count: AtomicU32::new(0), fail_before: false }
243 }
244 fn fail_before_call() -> Self {
245 Self { before_count: AtomicU32::new(0), after_count: AtomicU32::new(0), fail_before: true }
246 }
247 }
248 #[async_trait]
249 impl ToolPolicy for TrackingPolicy {
250 async fn evaluate_approval(&self, _: &str, _: &Value) -> Option<crate::types::ApprovalRequest> { None }
251 fn before_call(&self, _name: &str, _args: &Value, _ctx: &ToolContext) -> AgentResult<()> {
252 self.before_count.fetch_add(1, Ordering::SeqCst);
253 if self.fail_before {
254 return Err(AgentError::internal("before_call denied"));
255 }
256 Ok(())
257 }
258 fn after_call(&self, _name: &str, _args: &Value, _output: &ToolOutput, _ctx: &ToolContext) -> AgentResult<()> {
259 self.after_count.fetch_add(1, Ordering::SeqCst);
260 Ok(())
261 }
262 }
263
264 #[tokio::test]
267 async fn basic_execution() {
268 let pipeline = DefaultPipeline::new(None, None, None);
269 let output = pipeline.execute(&EchoTool, &json!({"msg": "hello"}), &test_ctx()).await.unwrap();
270 assert_eq!(output.summary, "hello");
271 }
272
273 #[tokio::test]
274 async fn policy_before_and_after_called() {
275 let policy = Arc::new(TrackingPolicy::new());
276 let pipeline = DefaultPipeline::new(Some(policy.clone()), None, None);
277
278 pipeline.execute(&EchoTool, &json!({}), &test_ctx()).await.unwrap();
279
280 assert_eq!(policy.before_count.load(Ordering::SeqCst), 1);
281 assert_eq!(policy.after_count.load(Ordering::SeqCst), 1);
282 }
283
284 #[tokio::test]
285 async fn policy_before_call_aborts() {
286 let policy = Arc::new(TrackingPolicy::fail_before_call());
287 let pipeline = DefaultPipeline::new(Some(policy.clone()), None, None);
288
289 let result = pipeline.execute(&EchoTool, &json!({}), &test_ctx()).await;
290 assert!(result.is_err());
291 assert_eq!(policy.after_count.load(Ordering::SeqCst), 0);
293 }
294
295 #[tokio::test]
296 async fn timeout_fires() {
297 let pipeline = DefaultPipeline::new(None, Some(50), None); let output = pipeline.execute(&SlowTool, &json!({}), &test_ctx()).await.unwrap();
299 assert_eq!(output.summary, "[Tool Timeout]");
300 }
301
302 #[tokio::test]
303 async fn no_timeout_when_tool_fast() {
304 let pipeline = DefaultPipeline::new(None, Some(5000), None);
305 let output = pipeline.execute(&EchoTool, &json!({"msg": "fast"}), &test_ctx()).await.unwrap();
306 assert_eq!(output.summary, "fast");
307 }
308
309 #[tokio::test]
310 async fn truncation_applies() {
311 let pipeline = DefaultPipeline::new(None, None, Some(10)); let output = pipeline.execute(&EchoTool, &json!({"msg": "this is a very long message"}), &test_ctx()).await.unwrap();
313 assert!(output.summary.len() <= 10);
314 assert!(output.truncation.is_some());
315 let t = output.truncation.unwrap();
316 assert_eq!(t.original_summary_len, 27);
317 assert_eq!(t.max_allowed_chars, 10);
318 }
319
320 #[tokio::test]
321 async fn no_truncation_when_short() {
322 let pipeline = DefaultPipeline::new(None, None, Some(100));
323 let output = pipeline.execute(&EchoTool, &json!({"msg": "short"}), &test_ctx()).await.unwrap();
324 assert_eq!(output.summary, "short");
325 assert!(output.truncation.is_none());
326 }
327
328 #[tokio::test]
329 async fn truncation_cjk_no_panic() {
330 let pipeline = DefaultPipeline::new(None, None, Some(20));
334 let output = pipeline.execute(&EchoTool, &json!({"msg": "这是一个很长的中文消息,用于测试多字节字符的截断处理"}), &test_ctx()).await.unwrap();
335 assert!(output.summary.len() <= 20);
336 assert!(output.summary.ends_with("...(truncated)") || output.summary.len() <= 20);
337 assert!(output.truncation.is_some());
338 }
340
341 #[tokio::test]
342 async fn timeout_plus_truncation() {
343 let pipeline = DefaultPipeline::new(None, Some(50), Some(100));
344 let output = pipeline.execute(&SlowTool, &json!({}), &test_ctx()).await.unwrap();
345 assert_eq!(output.summary, "[Tool Timeout]");
346 assert!(output.truncation.is_none()); }
348
349 #[tokio::test]
350 async fn tool_error_propagates() {
351 let pipeline = DefaultPipeline::new(None, None, None);
352 let result = pipeline.execute(&FailingTool, &json!({}), &test_ctx()).await;
353 assert!(result.is_err());
354 }
355
356 #[tokio::test]
359 async fn emits_start_and_finish_events() {
360 let inner = DefaultPipeline::new(None, None, None);
361 let event_bus = EventBus::new(64);
362 let mut rx = event_bus.subscribe();
363 let pipeline = EventEmittingPipeline::new(inner, event_bus);
364
365 let _ = pipeline.execute(&EchoTool, &json!({"msg": "test"}), &test_ctx()).await;
366
367 let mut events = Vec::new();
368 while let Ok(event) = rx.try_recv() {
369 events.push(event);
370 }
371 assert_eq!(events.len(), 2);
372
373 match &events[0] {
374 RuntimeEvent::ToolCallStarted { tool_name, .. } => assert_eq!(tool_name, "echo"),
375 _ => panic!("expected ToolCallStarted"),
376 }
377 match &events[1] {
378 RuntimeEvent::ToolCallFinished { tool_name, summary, .. } => {
379 assert_eq!(tool_name, "echo");
380 assert_eq!(summary, "test");
381 }
382 _ => panic!("expected ToolCallFinished"),
383 }
384 }
385
386 #[tokio::test]
387 async fn emits_finish_with_error_on_failure() {
388 let inner = DefaultPipeline::new(None, None, None);
389 let event_bus = EventBus::new(64);
390 let mut rx = event_bus.subscribe();
391 let pipeline = EventEmittingPipeline::new(inner, event_bus);
392
393 let _ = pipeline.execute(&FailingTool, &json!({}), &test_ctx()).await;
394
395 let mut events = Vec::new();
396 while let Ok(event) = rx.try_recv() {
397 events.push(event);
398 }
399 assert_eq!(events.len(), 2);
400
401 match &events[1] {
402 RuntimeEvent::ToolCallFinished { summary, .. } => {
403 assert!(summary.contains("intentional"));
404 }
405 _ => panic!("expected ToolCallFinished"),
406 }
407 }
408
409 #[tokio::test]
410 async fn event_emitting_delegates_to_inner() {
411 let policy = Arc::new(TrackingPolicy::new());
412 let inner = DefaultPipeline::new(Some(policy.clone()), None, None);
413 let event_bus = EventBus::new(64);
414 let pipeline = EventEmittingPipeline::new(inner, event_bus);
415
416 let output = pipeline.execute(&EchoTool, &json!({"msg": "delegated"}), &test_ctx()).await.unwrap();
417 assert_eq!(output.summary, "delegated");
418 assert_eq!(policy.before_count.load(Ordering::SeqCst), 1);
419 assert_eq!(policy.after_count.load(Ordering::SeqCst), 1);
420 }
421}