Skip to main content

awaken_runtime/execution/
executor.rs

1//! Tool execution strategies: Sequential and Parallel.
2
3use 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
15/// Result of executing a single tool call.
16pub struct ToolExecutionResult {
17    pub call: ToolCall,
18    pub result: ToolResult,
19    pub outcome: ToolCallOutcome,
20    /// Side-effects produced by the tool (state mutations, scheduled actions, effects).
21    pub command: StateCommand,
22}
23
24/// Error from tool execution strategy.
25#[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/// Strategy abstraction for tool execution.
34#[async_trait]
35pub trait ToolExecutor: Send + Sync {
36    /// Execute tool calls and return results.
37    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    /// Strategy name for logging.
45    fn name(&self) -> &'static str;
46
47    /// Whether the executor needs state refreshed between individual tool calls.
48    /// Sequential executors return true; parallel executors return false.
49    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/// Execute tool calls one by one in call order.
88/// Context freshness between calls is controlled by the caller.
89/// Stops at first suspension.
90#[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            // Stop at first suspension
118            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/// Policy controlling when resume decisions are replayed into tool execution.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum DecisionReplayPolicy {
141    /// Replay each resolved suspended call as soon as its decision arrives.
142    Immediate,
143    /// Replay only when all currently suspended calls have decisions.
144    BatchAllSuspended,
145}
146
147/// Parallel execution mode.
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum ParallelMode {
150    /// All concurrently, batch approval gate — wait for all decisions.
151    BatchApproval,
152    /// All concurrently, streaming results — replay decisions immediately.
153    Streaming,
154}
155
156/// Execute all tool calls concurrently. All tools see the same frozen snapshot.
157///
158/// Two modes differ in how suspension decisions are replayed:
159/// - `BatchApproval`: wait for all suspended calls to have decisions before replay
160/// - `Streaming`: replay each decision immediately as it arrives
161#[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    /// How the runtime should replay resolved suspend decisions.
180    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    /// Whether the runtime should enforce parallel patch conflict checks.
188    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
241/// Execute a single tool call, never panicking.
242pub(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    // -- Sequential tests --
376
377    #[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    // -- Parallel tests --
455
456    #[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        // Parallel executes ALL tools regardless of suspension
517        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    // -----------------------------------------------------------------------
623    // Migrated from uncarve: additional tool executor tests
624    // -----------------------------------------------------------------------
625
626    /// A tool that counts how many times it's been called.
627    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        // Verify order is preserved
677        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        // Unlike suspension, failures do NOT stop sequential execution
686        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    /// Validate that tool args are validated before execution.
782    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!({}))]; // missing required_field
810        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    // -----------------------------------------------------------------------
874    // Additional coverage: context preservation, mixed scenarios, edge cases
875    // -----------------------------------------------------------------------
876
877    /// Tool that captures the context it receives for later inspection.
878    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        // execute_single_tool sets call_id and tool_name from the call's context
919        // (the caller is responsible for setting ctx fields, which the executor does)
920    }
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        // The executor sets ctx.call_id = call.id before passing to execute_single_tool
978        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        // success, fail, suspend — suspension stops execution
989        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        // join_all preserves order of futures
1022        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}