1use async_trait::async_trait;
4use awaken_runtime_contract::contract::message::ToolCall;
5use awaken_runtime_contract::contract::suspension::ToolCallOutcome;
6use awaken_runtime_contract::contract::tool::{Tool, ToolCallContext, ToolOutput, ToolResult};
7use awaken_runtime_contract::state::StateCommand;
8use std::collections::HashMap;
9use std::sync::Arc;
10
11use super::tool_error::tool_error_result;
12#[cfg(feature = "background")]
13use crate::extensions::background::{ToolLineageContext, scope_tool_lineage_context};
14
15pub struct ToolExecutionResult {
17 pub call: ToolCall,
18 pub result: ToolResult,
19 pub outcome: ToolCallOutcome,
20 pub command: StateCommand,
22}
23
24#[derive(Debug, thiserror::Error)]
26pub enum ToolExecutorError {
27 #[error("tool execution cancelled")]
28 Cancelled,
29 #[error("tool execution failed: {0}")]
30 Failed(String),
31}
32
33#[async_trait]
35pub trait ToolExecutor: Send + Sync {
36 async fn execute(
38 &self,
39 tools: &HashMap<String, Arc<dyn Tool>>,
40 calls: &[ToolCall],
41 base_ctx: &ToolCallContext,
42 ) -> Result<Vec<ToolExecutionResult>, ToolExecutorError>;
43
44 fn name(&self) -> &'static str;
46
47 fn requires_incremental_state(&self) -> bool {
50 false
51 }
52}
53
54#[async_trait]
55pub trait ToolExecutionLocus: Send + Sync {
56 async fn execute_tool(
57 &self,
58 tool: &Arc<dyn Tool>,
59 call: &ToolCall,
60 ctx: &ToolCallContext,
61 ) -> Result<ToolOutput, awaken_runtime_contract::contract::tool::ToolError>;
62
63 fn name(&self) -> &'static str;
64}
65
66#[derive(Debug, Clone, Copy, Default)]
67pub struct DirectToolExecutionLocus;
68
69#[async_trait]
70impl ToolExecutionLocus for DirectToolExecutionLocus {
71 async fn execute_tool(
72 &self,
73 tool: &Arc<dyn Tool>,
74 call: &ToolCall,
75 ctx: &ToolCallContext,
76 ) -> Result<ToolOutput, awaken_runtime_contract::contract::tool::ToolError> {
77 execute_tool_with_runtime_context(tool, call, ctx).await
78 }
79
80 fn name(&self) -> &'static str {
81 "direct"
82 }
83}
84
85static DIRECT_TOOL_EXECUTION_LOCUS: DirectToolExecutionLocus = DirectToolExecutionLocus;
86
87#[derive(Debug, Clone, Copy, Default)]
91pub struct SequentialToolExecutor;
92
93#[async_trait]
94impl ToolExecutor for SequentialToolExecutor {
95 async fn execute(
96 &self,
97 tools: &HashMap<String, Arc<dyn Tool>>,
98 calls: &[ToolCall],
99 base_ctx: &ToolCallContext,
100 ) -> Result<Vec<ToolExecutionResult>, ToolExecutorError> {
101 let mut results = Vec::with_capacity(calls.len());
102
103 for call in calls {
104 let mut ctx = base_ctx.clone();
105 ctx.call_id = call.id.clone();
106 ctx.tool_name = call.name.clone();
107 let output = execute_single_tool(tools, call, &ctx).await;
108 let outcome = ToolCallOutcome::from_tool_result(&output.result);
109
110 results.push(ToolExecutionResult {
111 call: call.clone(),
112 result: output.result,
113 outcome,
114 command: output.command,
115 });
116
117 if results
119 .last()
120 .is_some_and(|r| r.outcome == ToolCallOutcome::Suspended)
121 {
122 break;
123 }
124 }
125
126 Ok(results)
127 }
128
129 fn name(&self) -> &'static str {
130 "sequential"
131 }
132
133 fn requires_incremental_state(&self) -> bool {
134 true
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum DecisionReplayPolicy {
141 Immediate,
143 BatchAllSuspended,
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum ParallelMode {
150 BatchApproval,
152 Streaming,
154}
155
156#[derive(Debug, Clone, Copy)]
162pub struct ParallelToolExecutor {
163 mode: ParallelMode,
164}
165
166impl ParallelToolExecutor {
167 pub const fn batch_approval() -> Self {
168 Self {
169 mode: ParallelMode::BatchApproval,
170 }
171 }
172
173 pub const fn streaming() -> Self {
174 Self {
175 mode: ParallelMode::Streaming,
176 }
177 }
178
179 pub fn decision_replay_policy(&self) -> DecisionReplayPolicy {
181 match self.mode {
182 ParallelMode::BatchApproval => DecisionReplayPolicy::BatchAllSuspended,
183 ParallelMode::Streaming => DecisionReplayPolicy::Immediate,
184 }
185 }
186
187 pub fn requires_conflict_check(&self) -> bool {
189 true
190 }
191}
192
193impl Default for ParallelToolExecutor {
194 fn default() -> Self {
195 Self::streaming()
196 }
197}
198
199#[async_trait]
200impl ToolExecutor for ParallelToolExecutor {
201 async fn execute(
202 &self,
203 tools: &HashMap<String, Arc<dyn Tool>>,
204 calls: &[ToolCall],
205 base_ctx: &ToolCallContext,
206 ) -> Result<Vec<ToolExecutionResult>, ToolExecutorError> {
207 use futures::future::join_all;
208
209 let futures: Vec<_> = calls
210 .iter()
211 .map(|call| {
212 let tools = tools.clone();
213 let call = call.clone();
214 let mut ctx = base_ctx.clone();
215 ctx.call_id = call.id.clone();
216 ctx.tool_name = call.name.clone();
217 async move {
218 let output = execute_single_tool(&tools, &call, &ctx).await;
219 let outcome = ToolCallOutcome::from_tool_result(&output.result);
220 ToolExecutionResult {
221 call,
222 result: output.result,
223 outcome,
224 command: output.command,
225 }
226 }
227 })
228 .collect();
229
230 Ok(join_all(futures).await)
231 }
232
233 fn name(&self) -> &'static str {
234 match self.mode {
235 ParallelMode::BatchApproval => "parallel_batch_approval",
236 ParallelMode::Streaming => "parallel_streaming",
237 }
238 }
239}
240
241pub(crate) async fn execute_single_tool(
243 tools: &HashMap<String, Arc<dyn Tool>>,
244 call: &ToolCall,
245 ctx: &ToolCallContext,
246) -> ToolOutput {
247 execute_single_tool_at(&DIRECT_TOOL_EXECUTION_LOCUS, tools, call, ctx).await
248}
249
250pub(crate) async fn execute_single_tool_at(
251 locus: &dyn ToolExecutionLocus,
252 tools: &HashMap<String, Arc<dyn Tool>>,
253 call: &ToolCall,
254 ctx: &ToolCallContext,
255) -> ToolOutput {
256 let Some(tool) = tools.get(&call.name) else {
257 return ToolResult::error(&call.name, format!("tool '{}' not found", call.name)).into();
258 };
259
260 if let Err(e) = tool.validate_args(&call.arguments) {
261 return ToolResult::error(&call.name, e.to_string()).into();
262 }
263
264 match locus.execute_tool(tool, call, ctx).await {
265 Ok(output) => output,
266 Err(e) => tool_error_result(&call.name, e).into(),
267 }
268}
269
270#[cfg(feature = "background")]
271async fn execute_tool_with_runtime_context(
272 tool: &Arc<dyn Tool>,
273 call: &ToolCall,
274 ctx: &ToolCallContext,
275) -> Result<ToolOutput, awaken_runtime_contract::contract::tool::ToolError> {
276 scope_tool_lineage_context(
277 ToolLineageContext {
278 run_id: ctx.run_identity.run_id.clone(),
279 call_id: ctx.call_id.clone(),
280 agent_id: ctx.run_identity.agent_id.clone(),
281 },
282 tool.execute(call.arguments.clone(), ctx),
283 )
284 .await
285}
286
287#[cfg(not(feature = "background"))]
288async fn execute_tool_with_runtime_context(
289 tool: &Arc<dyn Tool>,
290 call: &ToolCall,
291 ctx: &ToolCallContext,
292) -> Result<ToolOutput, awaken_runtime_contract::contract::tool::ToolError> {
293 tool.execute(call.arguments.clone(), ctx).await
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use awaken_runtime_contract::contract::tool::{ToolDescriptor, ToolError, ToolOutput};
300 use serde_json::{Value, json};
301
302 #[cfg(feature = "background")]
303 use crate::extensions::background::{
304 BackgroundTaskManager, BackgroundTaskPlugin, TaskParentContext, TaskResult,
305 };
306 #[cfg(feature = "background")]
307 use crate::phase::ExecutionEnv;
308 #[cfg(feature = "background")]
309 use crate::plugins::Plugin;
310 #[cfg(feature = "background")]
311 use crate::state::StateStore;
312 #[cfg(feature = "background")]
313 use awaken_runtime_contract::contract::identity::{RunIdentity, RunOrigin};
314
315 struct EchoTool;
316
317 #[async_trait]
318 impl Tool for EchoTool {
319 fn descriptor(&self) -> ToolDescriptor {
320 ToolDescriptor::new("echo", "echo", "Echoes input")
321 }
322
323 async fn execute(
324 &self,
325 args: Value,
326 _ctx: &ToolCallContext,
327 ) -> Result<ToolOutput, ToolError> {
328 let msg = args
329 .get("message")
330 .and_then(|v| v.as_str())
331 .unwrap_or("no message")
332 .to_string();
333 Ok(ToolResult::success_with_message("echo", args, msg).into())
334 }
335 }
336
337 struct FailingTool;
338
339 #[async_trait]
340 impl Tool for FailingTool {
341 fn descriptor(&self) -> ToolDescriptor {
342 ToolDescriptor::new("failing", "failing", "Always fails")
343 }
344
345 async fn execute(
346 &self,
347 _args: Value,
348 _ctx: &ToolCallContext,
349 ) -> Result<ToolOutput, ToolError> {
350 Err(ToolError::ExecutionFailed("intentional failure".into()))
351 }
352 }
353
354 struct SuspendingTool;
355
356 #[async_trait]
357 impl Tool for SuspendingTool {
358 fn descriptor(&self) -> ToolDescriptor {
359 ToolDescriptor::new("suspending", "suspending", "Returns pending")
360 }
361
362 async fn execute(
363 &self,
364 _args: Value,
365 _ctx: &ToolCallContext,
366 ) -> Result<ToolOutput, ToolError> {
367 Ok(ToolResult::suspended("suspending", "needs approval").into())
368 }
369 }
370
371 fn tool_map(tools: Vec<Arc<dyn Tool>>) -> HashMap<String, Arc<dyn Tool>> {
372 tools.into_iter().map(|t| (t.descriptor().id, t)).collect()
373 }
374
375 #[tokio::test]
378 async fn sequential_single_tool_success() {
379 let tools = tool_map(vec![Arc::new(EchoTool)]);
380 let calls = vec![ToolCall::new("c1", "echo", json!({"message": "hi"}))];
381 let executor = SequentialToolExecutor;
382
383 let results = executor
384 .execute(&tools, &calls, &ToolCallContext::test_default())
385 .await
386 .unwrap();
387 assert_eq!(results.len(), 1);
388 assert_eq!(results[0].outcome, ToolCallOutcome::Succeeded);
389 assert!(results[0].result.is_success());
390 }
391
392 #[tokio::test]
393 async fn sequential_partial_failure() {
394 let tools = tool_map(vec![Arc::new(EchoTool), Arc::new(FailingTool)]);
395 let calls = vec![
396 ToolCall::new("c1", "echo", json!({"message": "ok"})),
397 ToolCall::new("c2", "failing", json!({})),
398 ];
399 let executor = SequentialToolExecutor;
400
401 let results = executor
402 .execute(&tools, &calls, &ToolCallContext::test_default())
403 .await
404 .unwrap();
405 assert_eq!(results.len(), 2);
406 assert_eq!(results[0].outcome, ToolCallOutcome::Succeeded);
407 assert_eq!(results[1].outcome, ToolCallOutcome::Failed);
408 }
409
410 #[tokio::test]
411 async fn sequential_stops_after_first_suspension() {
412 let tools = tool_map(vec![Arc::new(SuspendingTool), Arc::new(EchoTool)]);
413 let calls = vec![
414 ToolCall::new("c1", "suspending", json!({})),
415 ToolCall::new("c2", "echo", json!({"message": "should not run"})),
416 ];
417 let executor = SequentialToolExecutor;
418
419 let results = executor
420 .execute(&tools, &calls, &ToolCallContext::test_default())
421 .await
422 .unwrap();
423 assert_eq!(results.len(), 1, "should stop after suspended tool");
424 assert_eq!(results[0].outcome, ToolCallOutcome::Suspended);
425 }
426
427 #[tokio::test]
428 async fn sequential_unknown_tool_returns_error() {
429 let tools = tool_map(vec![]);
430 let calls = vec![ToolCall::new("c1", "nonexistent", json!({}))];
431 let executor = SequentialToolExecutor;
432
433 let results = executor
434 .execute(&tools, &calls, &ToolCallContext::test_default())
435 .await
436 .unwrap();
437 assert_eq!(results.len(), 1);
438 assert_eq!(results[0].outcome, ToolCallOutcome::Failed);
439 assert!(results[0].result.is_error());
440 }
441
442 #[tokio::test]
443 async fn sequential_empty_calls() {
444 let tools = tool_map(vec![Arc::new(EchoTool)]);
445 let executor = SequentialToolExecutor;
446
447 let results = executor
448 .execute(&tools, &[], &ToolCallContext::test_default())
449 .await
450 .unwrap();
451 assert!(results.is_empty());
452 }
453
454 #[tokio::test]
457 async fn parallel_all_succeed() {
458 let tools = tool_map(vec![Arc::new(EchoTool)]);
459 let calls = vec![
460 ToolCall::new("c1", "echo", json!({"message": "first"})),
461 ToolCall::new("c2", "echo", json!({"message": "second"})),
462 ];
463 let executor = ParallelToolExecutor::streaming();
464
465 let results = executor
466 .execute(&tools, &calls, &ToolCallContext::test_default())
467 .await
468 .unwrap();
469 assert_eq!(results.len(), 2);
470 assert!(
471 results
472 .iter()
473 .all(|r| r.outcome == ToolCallOutcome::Succeeded)
474 );
475 }
476
477 #[tokio::test]
478 async fn parallel_partial_failure() {
479 let tools = tool_map(vec![Arc::new(EchoTool), Arc::new(FailingTool)]);
480 let calls = vec![
481 ToolCall::new("c1", "echo", json!({"message": "ok"})),
482 ToolCall::new("c2", "failing", json!({})),
483 ];
484 let executor = ParallelToolExecutor::streaming();
485
486 let results = executor
487 .execute(&tools, &calls, &ToolCallContext::test_default())
488 .await
489 .unwrap();
490 assert_eq!(results.len(), 2);
491 let successes = results
492 .iter()
493 .filter(|r| r.outcome == ToolCallOutcome::Succeeded)
494 .count();
495 let failures = results
496 .iter()
497 .filter(|r| r.outcome == ToolCallOutcome::Failed)
498 .count();
499 assert_eq!(successes, 1);
500 assert_eq!(failures, 1);
501 }
502
503 #[tokio::test]
504 async fn parallel_does_not_stop_on_suspension() {
505 let tools = tool_map(vec![Arc::new(SuspendingTool), Arc::new(EchoTool)]);
506 let calls = vec![
507 ToolCall::new("c1", "suspending", json!({})),
508 ToolCall::new("c2", "echo", json!({"message": "runs anyway"})),
509 ];
510 let executor = ParallelToolExecutor::streaming();
511
512 let results = executor
513 .execute(&tools, &calls, &ToolCallContext::test_default())
514 .await
515 .unwrap();
516 assert_eq!(results.len(), 2);
518 let suspended = results
519 .iter()
520 .filter(|r| r.outcome == ToolCallOutcome::Suspended)
521 .count();
522 let succeeded = results
523 .iter()
524 .filter(|r| r.outcome == ToolCallOutcome::Succeeded)
525 .count();
526 assert_eq!(suspended, 1);
527 assert_eq!(succeeded, 1);
528 }
529
530 #[tokio::test]
531 async fn parallel_empty_calls() {
532 let tools = tool_map(vec![Arc::new(EchoTool)]);
533 let executor = ParallelToolExecutor::streaming();
534
535 let results = executor
536 .execute(&tools, &[], &ToolCallContext::test_default())
537 .await
538 .unwrap();
539 assert!(results.is_empty());
540 }
541
542 #[test]
543 fn executor_names() {
544 assert_eq!(SequentialToolExecutor.name(), "sequential");
545 assert_eq!(
546 ParallelToolExecutor::streaming().name(),
547 "parallel_streaming"
548 );
549 assert_eq!(
550 ParallelToolExecutor::batch_approval().name(),
551 "parallel_batch_approval"
552 );
553 }
554
555 #[test]
556 fn parallel_default_is_streaming() {
557 let executor = ParallelToolExecutor::default();
558 assert_eq!(executor.name(), "parallel_streaming");
559 assert_eq!(
560 executor.decision_replay_policy(),
561 DecisionReplayPolicy::Immediate
562 );
563 }
564
565 #[test]
566 fn parallel_batch_approval_policy() {
567 let executor = ParallelToolExecutor::batch_approval();
568 assert_eq!(
569 executor.decision_replay_policy(),
570 DecisionReplayPolicy::BatchAllSuspended
571 );
572 assert!(executor.requires_conflict_check());
573 }
574
575 #[test]
576 fn parallel_streaming_policy() {
577 let executor = ParallelToolExecutor::streaming();
578 assert_eq!(
579 executor.decision_replay_policy(),
580 DecisionReplayPolicy::Immediate
581 );
582 assert!(executor.requires_conflict_check());
583 }
584
585 #[tokio::test]
586 async fn batch_approval_executes_all_concurrently() {
587 let tools = tool_map(vec![Arc::new(EchoTool)]);
588 let calls = vec![
589 ToolCall::new("c1", "echo", json!({"message": "a"})),
590 ToolCall::new("c2", "echo", json!({"message": "b"})),
591 ];
592 let executor = ParallelToolExecutor::batch_approval();
593
594 let results = executor
595 .execute(&tools, &calls, &ToolCallContext::test_default())
596 .await
597 .unwrap();
598 assert_eq!(results.len(), 2);
599 assert!(
600 results
601 .iter()
602 .all(|r| r.outcome == ToolCallOutcome::Succeeded)
603 );
604 }
605
606 #[tokio::test]
607 async fn batch_approval_does_not_stop_on_suspension() {
608 let tools = tool_map(vec![Arc::new(SuspendingTool), Arc::new(EchoTool)]);
609 let calls = vec![
610 ToolCall::new("c1", "suspending", json!({})),
611 ToolCall::new("c2", "echo", json!({"message": "runs anyway"})),
612 ];
613 let executor = ParallelToolExecutor::batch_approval();
614
615 let results = executor
616 .execute(&tools, &calls, &ToolCallContext::test_default())
617 .await
618 .unwrap();
619 assert_eq!(results.len(), 2);
620 }
621
622 struct CountingTool {
628 count: Arc<std::sync::atomic::AtomicUsize>,
629 }
630
631 impl CountingTool {
632 fn new() -> Self {
633 Self {
634 count: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
635 }
636 }
637
638 fn call_count(&self) -> usize {
639 self.count.load(std::sync::atomic::Ordering::SeqCst)
640 }
641 }
642
643 #[async_trait]
644 impl Tool for CountingTool {
645 fn descriptor(&self) -> ToolDescriptor {
646 ToolDescriptor::new("counting", "counting", "Counts calls")
647 }
648
649 async fn execute(
650 &self,
651 _args: Value,
652 _ctx: &ToolCallContext,
653 ) -> Result<ToolOutput, ToolError> {
654 let n = self.count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
655 Ok(ToolResult::success("counting", json!({"call_number": n + 1})).into())
656 }
657 }
658
659 #[tokio::test]
660 async fn sequential_multiple_calls_ordered() {
661 let counting = Arc::new(CountingTool::new());
662 let tools = tool_map(vec![counting.clone() as Arc<dyn Tool>]);
663 let calls = vec![
664 ToolCall::new("c1", "counting", json!({})),
665 ToolCall::new("c2", "counting", json!({})),
666 ToolCall::new("c3", "counting", json!({})),
667 ];
668 let executor = SequentialToolExecutor;
669
670 let results = executor
671 .execute(&tools, &calls, &ToolCallContext::test_default())
672 .await
673 .unwrap();
674 assert_eq!(results.len(), 3);
675 assert_eq!(counting.call_count(), 3);
676 for (i, result) in results.iter().enumerate() {
678 assert_eq!(result.call.id, format!("c{}", i + 1));
679 assert_eq!(result.outcome, ToolCallOutcome::Succeeded);
680 }
681 }
682
683 #[tokio::test]
684 async fn sequential_failure_does_not_stop_execution() {
685 let tools = tool_map(vec![Arc::new(FailingTool), Arc::new(EchoTool)]);
687 let calls = vec![
688 ToolCall::new("c1", "failing", json!({})),
689 ToolCall::new("c2", "echo", json!({"message": "still runs"})),
690 ];
691 let executor = SequentialToolExecutor;
692
693 let results = executor
694 .execute(&tools, &calls, &ToolCallContext::test_default())
695 .await
696 .unwrap();
697 assert_eq!(results.len(), 2);
698 assert_eq!(results[0].outcome, ToolCallOutcome::Failed);
699 assert_eq!(results[1].outcome, ToolCallOutcome::Succeeded);
700 }
701
702 #[tokio::test]
703 async fn sequential_suspension_in_middle_stops_remaining() {
704 let tools = tool_map(vec![
705 Arc::new(EchoTool),
706 Arc::new(SuspendingTool),
707 Arc::new(EchoTool),
708 ]);
709 let calls = vec![
710 ToolCall::new("c1", "echo", json!({"message": "first"})),
711 ToolCall::new("c2", "suspending", json!({})),
712 ToolCall::new("c3", "echo", json!({"message": "should not run"})),
713 ];
714 let executor = SequentialToolExecutor;
715
716 let results = executor
717 .execute(&tools, &calls, &ToolCallContext::test_default())
718 .await
719 .unwrap();
720 assert_eq!(results.len(), 2, "should stop after suspension");
721 assert_eq!(results[0].outcome, ToolCallOutcome::Succeeded);
722 assert_eq!(results[1].outcome, ToolCallOutcome::Suspended);
723 }
724
725 #[tokio::test]
726 async fn parallel_all_fail() {
727 let tools = tool_map(vec![Arc::new(FailingTool)]);
728 let calls = vec![
729 ToolCall::new("c1", "failing", json!({})),
730 ToolCall::new("c2", "failing", json!({})),
731 ];
732 let executor = ParallelToolExecutor::streaming();
733
734 let results = executor
735 .execute(&tools, &calls, &ToolCallContext::test_default())
736 .await
737 .unwrap();
738 assert_eq!(results.len(), 2);
739 assert!(results.iter().all(|r| r.outcome == ToolCallOutcome::Failed));
740 }
741
742 #[tokio::test]
743 async fn parallel_unknown_tool_returns_error() {
744 let tools = tool_map(vec![]);
745 let calls = vec![
746 ToolCall::new("c1", "nonexistent_a", json!({})),
747 ToolCall::new("c2", "nonexistent_b", json!({})),
748 ];
749 let executor = ParallelToolExecutor::streaming();
750
751 let results = executor
752 .execute(&tools, &calls, &ToolCallContext::test_default())
753 .await
754 .unwrap();
755 assert_eq!(results.len(), 2);
756 assert!(results.iter().all(|r| r.outcome == ToolCallOutcome::Failed));
757 for r in &results {
758 assert!(r.result.is_error());
759 }
760 }
761
762 #[tokio::test]
763 async fn parallel_counting_tool_all_called() {
764 let counting = Arc::new(CountingTool::new());
765 let tools = tool_map(vec![counting.clone() as Arc<dyn Tool>]);
766 let calls = vec![
767 ToolCall::new("c1", "counting", json!({})),
768 ToolCall::new("c2", "counting", json!({})),
769 ToolCall::new("c3", "counting", json!({})),
770 ];
771 let executor = ParallelToolExecutor::streaming();
772
773 let results = executor
774 .execute(&tools, &calls, &ToolCallContext::test_default())
775 .await
776 .unwrap();
777 assert_eq!(results.len(), 3);
778 assert_eq!(counting.call_count(), 3);
779 }
780
781 struct StrictArgsTool;
783
784 #[async_trait]
785 impl Tool for StrictArgsTool {
786 fn descriptor(&self) -> ToolDescriptor {
787 ToolDescriptor::new("strict", "strict", "Validates args")
788 }
789
790 fn validate_args(&self, args: &Value) -> Result<(), ToolError> {
791 if args.get("required_field").is_none() {
792 return Err(ToolError::InvalidArguments("missing required_field".into()));
793 }
794 Ok(())
795 }
796
797 async fn execute(
798 &self,
799 args: Value,
800 _ctx: &ToolCallContext,
801 ) -> Result<ToolOutput, ToolError> {
802 Ok(ToolResult::success("strict", args).into())
803 }
804 }
805
806 #[tokio::test]
807 async fn sequential_validates_args_before_execute() {
808 let tools = tool_map(vec![Arc::new(StrictArgsTool)]);
809 let calls = vec![ToolCall::new("c1", "strict", json!({}))]; let executor = SequentialToolExecutor;
811
812 let results = executor
813 .execute(&tools, &calls, &ToolCallContext::test_default())
814 .await
815 .unwrap();
816 assert_eq!(results.len(), 1);
817 assert_eq!(results[0].outcome, ToolCallOutcome::Failed);
818 assert!(results[0].result.is_error());
819 }
820
821 #[tokio::test]
822 async fn sequential_valid_args_succeeds() {
823 let tools = tool_map(vec![Arc::new(StrictArgsTool)]);
824 let calls = vec![ToolCall::new(
825 "c1",
826 "strict",
827 json!({"required_field": "present"}),
828 )];
829 let executor = SequentialToolExecutor;
830
831 let results = executor
832 .execute(&tools, &calls, &ToolCallContext::test_default())
833 .await
834 .unwrap();
835 assert_eq!(results.len(), 1);
836 assert_eq!(results[0].outcome, ToolCallOutcome::Succeeded);
837 }
838
839 #[tokio::test]
840 async fn parallel_validates_args_before_execute() {
841 let tools = tool_map(vec![Arc::new(StrictArgsTool)]);
842 let calls = vec![ToolCall::new("c1", "strict", json!({}))];
843 let executor = ParallelToolExecutor::streaming();
844
845 let results = executor
846 .execute(&tools, &calls, &ToolCallContext::test_default())
847 .await
848 .unwrap();
849 assert_eq!(results.len(), 1);
850 assert_eq!(results[0].outcome, ToolCallOutcome::Failed);
851 }
852
853 #[test]
854 fn tool_execution_result_fields() {
855 let result = ToolExecutionResult {
856 call: ToolCall::new("c1", "echo", json!({})),
857 result: ToolResult::success("echo", json!({"ok": true})),
858 outcome: ToolCallOutcome::Succeeded,
859 command: StateCommand::default(),
860 };
861 assert_eq!(result.call.id, "c1");
862 assert_eq!(result.outcome, ToolCallOutcome::Succeeded);
863 }
864
865 #[test]
866 fn tool_executor_error_display() {
867 let err = ToolExecutorError::Cancelled;
868 assert!(err.to_string().contains("cancelled"));
869 let err2 = ToolExecutorError::Failed("some reason".into());
870 assert!(err2.to_string().contains("some reason"));
871 }
872
873 struct ContextCaptureTool {
879 captured_call_id: Arc<std::sync::Mutex<String>>,
880 captured_tool_name: Arc<std::sync::Mutex<String>>,
881 }
882
883 impl ContextCaptureTool {
884 fn new() -> Self {
885 Self {
886 captured_call_id: Arc::new(std::sync::Mutex::new(String::new())),
887 captured_tool_name: Arc::new(std::sync::Mutex::new(String::new())),
888 }
889 }
890 }
891
892 #[async_trait]
893 impl Tool for ContextCaptureTool {
894 fn descriptor(&self) -> ToolDescriptor {
895 ToolDescriptor::new("capture", "capture", "Captures context")
896 }
897
898 async fn execute(
899 &self,
900 _args: Value,
901 ctx: &ToolCallContext,
902 ) -> Result<ToolOutput, ToolError> {
903 *self.captured_call_id.lock().unwrap() = ctx.call_id.clone();
904 *self.captured_tool_name.lock().unwrap() = ctx.tool_name.clone();
905 Ok(ToolResult::success("capture", json!({"captured": true})).into())
906 }
907 }
908
909 #[tokio::test]
910 async fn execute_single_tool_preserves_call_context() {
911 let capture = Arc::new(ContextCaptureTool::new());
912 let tools = tool_map(vec![capture.clone() as Arc<dyn Tool>]);
913 let call = ToolCall::new("call-42", "capture", json!({}));
914 let ctx = ToolCallContext::test_default();
915
916 let output = execute_single_tool(&tools, &call, &ctx).await;
917 assert!(output.result.is_success());
918 }
921
922 #[tokio::test]
923 async fn direct_tool_execution_locus_executes_existing_tool() {
924 let locus = DirectToolExecutionLocus;
925 assert_eq!(locus.name(), "direct");
926
927 let tools = tool_map(vec![Arc::new(EchoTool) as Arc<dyn Tool>]);
928 let call = ToolCall::new("call-direct", "echo", json!({"message": "ok"}));
929 let ctx = ToolCallContext::test_default();
930
931 let output = execute_single_tool_at(&locus, &tools, &call, &ctx).await;
932 assert!(output.result.is_success());
933 assert_eq!(output.result.message.as_deref(), Some("ok"));
934 }
935
936 #[tokio::test]
937 async fn execute_single_tool_missing_returns_error_with_name() {
938 let tools: HashMap<String, Arc<dyn Tool>> = HashMap::new();
939 let call = ToolCall::new("c1", "ghost_tool", json!({}));
940 let ctx = ToolCallContext::test_default();
941
942 let output = execute_single_tool(&tools, &call, &ctx).await;
943 assert!(output.result.is_error());
944 assert!(
945 output
946 .result
947 .message
948 .as_deref()
949 .unwrap_or("")
950 .contains("ghost_tool")
951 );
952 }
953
954 #[tokio::test]
955 async fn execute_single_tool_validates_args() {
956 let tools = tool_map(vec![Arc::new(StrictArgsTool)]);
957 let call = ToolCall::new("c1", "strict", json!({"wrong": "field"}));
958 let ctx = ToolCallContext::test_default();
959
960 let output = execute_single_tool(&tools, &call, &ctx).await;
961 assert!(output.result.is_error());
962 }
963
964 #[tokio::test]
965 async fn sequential_context_call_id_set_per_tool() {
966 let capture = Arc::new(ContextCaptureTool::new());
967 let tools = tool_map(vec![capture.clone() as Arc<dyn Tool>]);
968 let calls = vec![ToolCall::new("unique-id-99", "capture", json!({}))];
969 let executor = SequentialToolExecutor;
970
971 let results = executor
972 .execute(&tools, &calls, &ToolCallContext::test_default())
973 .await
974 .unwrap();
975 assert_eq!(results.len(), 1);
976 assert_eq!(results[0].call.id, "unique-id-99");
977 assert_eq!(*capture.captured_call_id.lock().unwrap(), "unique-id-99");
979 }
980
981 #[tokio::test]
982 async fn sequential_mixed_success_failure_suspension_order() {
983 let tools = tool_map(vec![
984 Arc::new(EchoTool),
985 Arc::new(FailingTool),
986 Arc::new(SuspendingTool),
987 ]);
988 let calls = vec![
990 ToolCall::new("c1", "echo", json!({"message": "hi"})),
991 ToolCall::new("c2", "failing", json!({})),
992 ToolCall::new("c3", "suspending", json!({})),
993 ToolCall::new("c4", "echo", json!({"message": "should not run"})),
994 ];
995 let executor = SequentialToolExecutor;
996
997 let results = executor
998 .execute(&tools, &calls, &ToolCallContext::test_default())
999 .await
1000 .unwrap();
1001 assert_eq!(results.len(), 3, "stops after suspension at c3");
1002 assert_eq!(results[0].outcome, ToolCallOutcome::Succeeded);
1003 assert_eq!(results[1].outcome, ToolCallOutcome::Failed);
1004 assert_eq!(results[2].outcome, ToolCallOutcome::Suspended);
1005 }
1006
1007 #[tokio::test]
1008 async fn parallel_preserves_result_order() {
1009 let counting = Arc::new(CountingTool::new());
1010 let tools = tool_map(vec![counting.clone() as Arc<dyn Tool>]);
1011 let calls: Vec<_> = (0..5)
1012 .map(|i| ToolCall::new(format!("c{i}"), "counting", json!({})))
1013 .collect();
1014 let executor = ParallelToolExecutor::streaming();
1015
1016 let results = executor
1017 .execute(&tools, &calls, &ToolCallContext::test_default())
1018 .await
1019 .unwrap();
1020 assert_eq!(results.len(), 5);
1021 for (i, r) in results.iter().enumerate() {
1023 assert_eq!(r.call.id, format!("c{i}"));
1024 }
1025 }
1026
1027 #[tokio::test]
1028 async fn parallel_mixed_success_failure_suspension() {
1029 let tools = tool_map(vec![
1030 Arc::new(EchoTool),
1031 Arc::new(FailingTool),
1032 Arc::new(SuspendingTool),
1033 ]);
1034 let calls = vec![
1035 ToolCall::new("c1", "echo", json!({"message": "hi"})),
1036 ToolCall::new("c2", "failing", json!({})),
1037 ToolCall::new("c3", "suspending", json!({})),
1038 ];
1039 let executor = ParallelToolExecutor::batch_approval();
1040
1041 let results = executor
1042 .execute(&tools, &calls, &ToolCallContext::test_default())
1043 .await
1044 .unwrap();
1045 assert_eq!(results.len(), 3, "parallel runs all regardless");
1046 assert_eq!(results[0].outcome, ToolCallOutcome::Succeeded);
1047 assert_eq!(results[1].outcome, ToolCallOutcome::Failed);
1048 assert_eq!(results[2].outcome, ToolCallOutcome::Suspended);
1049 }
1050
1051 #[test]
1052 fn sequential_requires_incremental_state() {
1053 let executor = SequentialToolExecutor;
1054 assert!(executor.requires_incremental_state());
1055 }
1056
1057 #[test]
1058 fn parallel_does_not_require_incremental_state() {
1059 let executor = ParallelToolExecutor::streaming();
1060 assert!(!executor.requires_incremental_state());
1061 let batch = ParallelToolExecutor::batch_approval();
1062 assert!(!batch.requires_incremental_state());
1063 }
1064
1065 #[tokio::test]
1066 async fn execute_single_tool_success_returns_correct_tool_name() {
1067 let tools = tool_map(vec![Arc::new(EchoTool)]);
1068 let call = ToolCall::new("c1", "echo", json!({"message": "test"}));
1069 let ctx = ToolCallContext::test_default();
1070
1071 let output = execute_single_tool(&tools, &call, &ctx).await;
1072 assert!(output.result.is_success());
1073 assert_eq!(output.result.tool_name, "echo");
1074 }
1075
1076 #[cfg(feature = "background")]
1077 struct SpawnBackgroundTaskTool {
1078 manager: Arc<BackgroundTaskManager>,
1079 spawned_task_id: Arc<std::sync::Mutex<Option<String>>>,
1080 }
1081
1082 #[cfg(feature = "background")]
1083 #[async_trait]
1084 impl Tool for SpawnBackgroundTaskTool {
1085 fn descriptor(&self) -> ToolDescriptor {
1086 ToolDescriptor::new(
1087 "spawn_background",
1088 "spawn_background",
1089 "Spawns a background task",
1090 )
1091 }
1092
1093 async fn execute(
1094 &self,
1095 _args: Value,
1096 _ctx: &ToolCallContext,
1097 ) -> Result<ToolOutput, ToolError> {
1098 let task_id = self
1099 .manager
1100 .spawn(
1101 "thread-1",
1102 "background",
1103 None,
1104 "spawned from tool",
1105 TaskParentContext::default(),
1106 |_| async { TaskResult::Success(json!({"ok": true})) },
1107 )
1108 .await
1109 .map_err(|error| ToolError::ExecutionFailed(error.to_string()))?;
1110 *self.spawned_task_id.lock().unwrap() = Some(task_id.clone());
1111 Ok(ToolResult::success("spawn_background", json!({"task_id": task_id})).into())
1112 }
1113 }
1114
1115 #[cfg(feature = "background")]
1116 #[tokio::test]
1117 async fn execute_single_tool_scopes_tool_lineage_for_background_spawns() {
1118 let store = StateStore::new();
1119 let manager = Arc::new(BackgroundTaskManager::new());
1120 manager.set_store(store.clone());
1121 let plugin: Arc<dyn Plugin> = Arc::new(BackgroundTaskPlugin::new(manager.clone()));
1122 let env = ExecutionEnv::from_plugins(&[plugin], &Default::default()).unwrap();
1123 store.register_keys(&env.key_registrations).unwrap();
1124
1125 let spawned_task_id = Arc::new(std::sync::Mutex::new(None::<String>));
1126 let tools = tool_map(vec![Arc::new(SpawnBackgroundTaskTool {
1127 manager: manager.clone(),
1128 spawned_task_id: spawned_task_id.clone(),
1129 }) as Arc<dyn Tool>]);
1130 let executor = SequentialToolExecutor;
1131 let calls = vec![ToolCall::new("call-77", "spawn_background", json!({}))];
1132
1133 let mut ctx = ToolCallContext::test_default();
1134 ctx.run_identity = RunIdentity::new(
1135 "thread-1".into(),
1136 None,
1137 "run-77".into(),
1138 None,
1139 "agent-77".into(),
1140 RunOrigin::User,
1141 );
1142 ctx.cancellation_token = Some(crate::cancellation::CancellationToken::new());
1143
1144 let results = executor.execute(&tools, &calls, &ctx).await.unwrap();
1145 assert_eq!(results.len(), 1);
1146 assert!(results[0].result.is_success());
1147
1148 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1149
1150 let task_id = spawned_task_id
1151 .lock()
1152 .unwrap()
1153 .clone()
1154 .expect("spawned task id should be recorded");
1155 let summary = manager
1156 .get(&task_id)
1157 .await
1158 .expect("spawned background task should be queryable");
1159 assert_eq!(summary.parent_context.run_id.as_deref(), Some("run-77"));
1160 assert_eq!(summary.parent_context.call_id.as_deref(), Some("call-77"));
1161 assert_eq!(summary.parent_context.agent_id.as_deref(), Some("agent-77"));
1162 }
1163}