use crate::executor::{ToolCall, ToolError, ToolExecutor, ToolOutput};
use crate::registry::ToolDef;
#[derive(Debug)]
pub struct CompositeExecutor<A: ToolExecutor, B: ToolExecutor> {
first: A,
second: B,
}
impl<A: ToolExecutor, B: ToolExecutor> CompositeExecutor<A, B> {
#[must_use]
pub fn new(first: A, second: B) -> Self {
Self { first, second }
}
}
impl<A: ToolExecutor, B: ToolExecutor> ToolExecutor for CompositeExecutor<A, B> {
async fn execute(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
if let Some(output) = self.first.execute(response).await? {
return Ok(Some(output));
}
self.second.execute(response).await
}
async fn execute_confirmed(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
if let Some(output) = self.first.execute_confirmed(response).await? {
return Ok(Some(output));
}
self.second.execute_confirmed(response).await
}
fn tool_definitions(&self) -> Vec<ToolDef> {
let mut defs = self.first.tool_definitions();
let seen: std::collections::HashSet<String> =
defs.iter().map(|d| d.id.to_string()).collect();
for def in self.second.tool_definitions() {
if !seen.contains(def.id.as_ref()) {
defs.push(def);
}
}
defs
}
async fn execute_tool_call(&self, call: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
if let Some(output) = self.first.execute_tool_call(call).await? {
return Ok(Some(output));
}
self.second.execute_tool_call(call).await
}
async fn execute_tool_call_confirmed(
&self,
call: &ToolCall,
) -> Result<Option<ToolOutput>, ToolError> {
if let Some(output) = self.first.execute_tool_call_confirmed(call).await? {
return Ok(Some(output));
}
self.second.execute_tool_call_confirmed(call).await
}
fn is_tool_retryable(&self, tool_id: &str) -> bool {
self.first.is_tool_retryable(tool_id) || self.second.is_tool_retryable(tool_id)
}
fn is_tool_speculatable(&self, tool_id: &str) -> bool {
self.first.is_tool_speculatable(tool_id) || self.second.is_tool_speculatable(tool_id)
}
fn requires_confirmation(&self, call: &ToolCall) -> bool {
self.first.requires_confirmation(call) || self.second.requires_confirmation(call)
}
fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
self.first.set_skill_env(env.clone());
self.second.set_skill_env(env);
}
fn set_effective_trust(&self, level: crate::SkillTrustLevel) {
self.first.set_effective_trust(level);
self.second.set_effective_trust(level);
}
fn checkpoint_undo(&self, n: usize) -> crate::executor::CheckpointActionResult {
let result = self.first.checkpoint_undo(n);
if result.supported {
return result;
}
self.second.checkpoint_undo(n)
}
fn checkpoint_redo(&self) -> crate::executor::CheckpointActionResult {
let result = self.first.checkpoint_redo();
if result.supported {
return result;
}
self.second.checkpoint_redo()
}
fn checkpoint_list(&self) -> crate::executor::CheckpointListResult {
let result = self.first.checkpoint_list();
if result.supported {
return result;
}
self.second.checkpoint_list()
}
}
#[derive(Debug)]
pub struct OptionalExecutor<T: ToolExecutor>(pub Option<T>);
impl<T: ToolExecutor> ToolExecutor for OptionalExecutor<T> {
async fn execute(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
match &self.0 {
Some(inner) => inner.execute(response).await,
None => Ok(None),
}
}
async fn execute_confirmed(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
match &self.0 {
Some(inner) => inner.execute_confirmed(response).await,
None => Ok(None),
}
}
fn tool_definitions(&self) -> Vec<ToolDef> {
self.0
.as_ref()
.map(ToolExecutor::tool_definitions)
.unwrap_or_default()
}
async fn execute_tool_call(&self, call: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
match &self.0 {
Some(inner) => inner.execute_tool_call(call).await,
None => Ok(None),
}
}
async fn execute_tool_call_confirmed(
&self,
call: &ToolCall,
) -> Result<Option<ToolOutput>, ToolError> {
match &self.0 {
Some(inner) => inner.execute_tool_call_confirmed(call).await,
None => Ok(None),
}
}
fn is_tool_retryable(&self, tool_id: &str) -> bool {
self.0
.as_ref()
.is_some_and(|inner| inner.is_tool_retryable(tool_id))
}
fn is_tool_speculatable(&self, tool_id: &str) -> bool {
self.0
.as_ref()
.is_some_and(|inner| inner.is_tool_speculatable(tool_id))
}
fn requires_confirmation(&self, call: &ToolCall) -> bool {
self.0
.as_ref()
.is_some_and(|inner| inner.requires_confirmation(call))
}
fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
if let Some(inner) = &self.0 {
inner.set_skill_env(env);
}
}
fn set_effective_trust(&self, level: crate::SkillTrustLevel) {
if let Some(inner) = &self.0 {
inner.set_effective_trust(level);
}
}
fn checkpoint_undo(&self, n: usize) -> crate::executor::CheckpointActionResult {
self.0.as_ref().map_or_else(
crate::executor::CheckpointActionResult::unsupported,
|inner| inner.checkpoint_undo(n),
)
}
fn checkpoint_redo(&self) -> crate::executor::CheckpointActionResult {
self.0.as_ref().map_or_else(
crate::executor::CheckpointActionResult::unsupported,
ToolExecutor::checkpoint_redo,
)
}
fn checkpoint_list(&self) -> crate::executor::CheckpointListResult {
self.0
.as_ref()
.map(ToolExecutor::checkpoint_list)
.unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ToolName;
use std::assert_matches;
#[derive(Debug)]
struct MatchingExecutor;
impl ToolExecutor for MatchingExecutor {
async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
Ok(Some(ToolOutput {
tool_name: ToolName::new("test"),
summary: "matched".to_owned(),
blocks_executed: 1,
filter_stats: None,
diff: None,
streamed: false,
terminal_id: None,
locations: None,
raw_response: None,
claim_source: None,
..Default::default()
}))
}
crate::tool_executor_no_inner_defaults!();
}
#[derive(Debug)]
struct NoMatchExecutor;
impl ToolExecutor for NoMatchExecutor {
async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
Ok(None)
}
crate::tool_executor_no_inner_defaults!();
}
#[derive(Debug)]
struct ErrorExecutor;
impl ToolExecutor for ErrorExecutor {
async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
Err(ToolError::Blocked {
command: "test".to_owned(),
})
}
crate::tool_executor_no_inner_defaults!();
}
#[derive(Debug)]
struct SecondExecutor;
impl ToolExecutor for SecondExecutor {
async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
Ok(Some(ToolOutput {
tool_name: ToolName::new("test"),
summary: "second".to_owned(),
blocks_executed: 1,
filter_stats: None,
diff: None,
streamed: false,
terminal_id: None,
locations: None,
raw_response: None,
claim_source: None,
..Default::default()
}))
}
crate::tool_executor_no_inner_defaults!();
}
#[tokio::test]
async fn first_matches_returns_first() {
let composite = CompositeExecutor::new(MatchingExecutor, SecondExecutor);
let result = composite.execute("anything").await.unwrap();
assert_eq!(result.unwrap().summary, "matched");
}
#[tokio::test]
async fn first_none_falls_through_to_second() {
let composite = CompositeExecutor::new(NoMatchExecutor, SecondExecutor);
let result = composite.execute("anything").await.unwrap();
assert_eq!(result.unwrap().summary, "second");
}
#[tokio::test]
async fn both_none_returns_none() {
let composite = CompositeExecutor::new(NoMatchExecutor, NoMatchExecutor);
let result = composite.execute("anything").await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn first_error_propagates_without_trying_second() {
let composite = CompositeExecutor::new(ErrorExecutor, SecondExecutor);
let result = composite.execute("anything").await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn second_error_propagates_when_first_none() {
let composite = CompositeExecutor::new(NoMatchExecutor, ErrorExecutor);
let result = composite.execute("anything").await;
assert_matches!(result, Err(ToolError::Blocked { .. }));
}
#[tokio::test]
async fn execute_confirmed_first_matches() {
let composite = CompositeExecutor::new(MatchingExecutor, SecondExecutor);
let result = composite.execute_confirmed("anything").await.unwrap();
assert_eq!(result.unwrap().summary, "matched");
}
#[tokio::test]
async fn execute_confirmed_falls_through() {
let composite = CompositeExecutor::new(NoMatchExecutor, SecondExecutor);
let result = composite.execute_confirmed("anything").await.unwrap();
assert_eq!(result.unwrap().summary, "second");
}
#[test]
fn composite_debug() {
let composite = CompositeExecutor::new(MatchingExecutor, SecondExecutor);
let debug = format!("{composite:?}");
assert!(debug.contains("CompositeExecutor"));
}
#[derive(Debug, Default)]
struct ConfirmedSpy {
confirmed_called: std::sync::Mutex<bool>,
unconfirmed_called: std::sync::Mutex<bool>,
}
impl ToolExecutor for ConfirmedSpy {
async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
Ok(None)
}
async fn execute_tool_call(
&self,
call: &ToolCall,
) -> Result<Option<ToolOutput>, ToolError> {
*self.unconfirmed_called.lock().unwrap() = true;
Ok(Some(ToolOutput {
tool_name: call.tool_id.clone(),
summary: "unconfirmed".to_owned(),
blocks_executed: 1,
filter_stats: None,
diff: None,
streamed: false,
terminal_id: None,
locations: None,
raw_response: None,
claim_source: None,
..Default::default()
}))
}
async fn execute_tool_call_confirmed(
&self,
call: &ToolCall,
) -> Result<Option<ToolOutput>, ToolError> {
*self.confirmed_called.lock().unwrap() = true;
Ok(Some(ToolOutput {
tool_name: call.tool_id.clone(),
summary: "confirmed".to_owned(),
blocks_executed: 1,
filter_stats: None,
diff: None,
streamed: false,
terminal_id: None,
locations: None,
raw_response: None,
claim_source: None,
..Default::default()
}))
}
fn checkpoint_undo(&self, _n: usize) -> crate::CheckpointActionResult {
crate::CheckpointActionResult::unsupported()
}
fn checkpoint_redo(&self) -> crate::CheckpointActionResult {
crate::CheckpointActionResult::unsupported()
}
fn checkpoint_list(&self) -> crate::CheckpointListResult {
crate::CheckpointListResult::default()
}
fn is_tool_speculatable(&self, _tool_id: &str) -> bool {
false
}
fn requires_confirmation(&self, _call: &ToolCall) -> bool {
false
}
}
#[tokio::test]
async fn execute_tool_call_confirmed_bypasses_unconfirmed_dispatch() {
let spy = ConfirmedSpy::default();
let composite = CompositeExecutor::new(spy, NoMatchExecutor);
let call = ToolCall {
tool_id: ToolName::new("read"),
params: serde_json::Map::new(),
caller_id: None,
context: None,
tool_call_id: String::new(),
skill_name: None,
};
let result = composite
.execute_tool_call_confirmed(&call)
.await
.unwrap()
.unwrap();
assert_eq!(result.summary, "confirmed");
assert!(
*composite.first.confirmed_called.lock().unwrap(),
"execute_tool_call_confirmed must reach the inner executor's confirmed override"
);
assert!(
!*composite.first.unconfirmed_called.lock().unwrap(),
"execute_tool_call_confirmed must NOT re-dispatch through execute_tool_call"
);
}
#[tokio::test]
async fn execute_tool_call_confirmed_falls_through_to_second() {
let composite = CompositeExecutor::new(NoMatchExecutor, ConfirmedSpy::default());
let call = ToolCall {
tool_id: ToolName::new("read"),
params: serde_json::Map::new(),
caller_id: None,
context: None,
tool_call_id: String::new(),
skill_name: None,
};
let result = composite
.execute_tool_call_confirmed(&call)
.await
.unwrap()
.unwrap();
assert_eq!(result.summary, "confirmed");
assert!(*composite.second.confirmed_called.lock().unwrap());
}
#[derive(Debug)]
struct FileToolExecutor;
impl ToolExecutor for FileToolExecutor {
async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
Ok(None)
}
async fn execute_tool_call(
&self,
call: &ToolCall,
) -> Result<Option<ToolOutput>, ToolError> {
if call.tool_id == "read" || call.tool_id == "write" {
Ok(Some(ToolOutput {
tool_name: call.tool_id.clone(),
summary: "file_handler".to_owned(),
blocks_executed: 1,
filter_stats: None,
diff: None,
streamed: false,
terminal_id: None,
locations: None,
raw_response: None,
claim_source: None,
..Default::default()
}))
} else {
Ok(None)
}
}
crate::tool_executor_no_inner_defaults!();
}
#[derive(Debug)]
struct ShellToolExecutor;
impl ToolExecutor for ShellToolExecutor {
async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
Ok(None)
}
async fn execute_tool_call(
&self,
call: &ToolCall,
) -> Result<Option<ToolOutput>, ToolError> {
if call.tool_id == "bash" {
Ok(Some(ToolOutput {
tool_name: ToolName::new("bash"),
summary: "shell_handler".to_owned(),
blocks_executed: 1,
filter_stats: None,
diff: None,
streamed: false,
terminal_id: None,
locations: None,
raw_response: None,
claim_source: None,
..Default::default()
}))
} else {
Ok(None)
}
}
crate::tool_executor_no_inner_defaults!();
}
#[tokio::test]
async fn tool_call_routes_to_file_executor() {
let composite = CompositeExecutor::new(FileToolExecutor, ShellToolExecutor);
let call = ToolCall {
tool_id: ToolName::new("read"),
params: serde_json::Map::new(),
caller_id: None,
context: None,
tool_call_id: String::new(),
skill_name: None,
};
let result = composite.execute_tool_call(&call).await.unwrap().unwrap();
assert_eq!(result.summary, "file_handler");
}
#[tokio::test]
async fn tool_call_routes_to_shell_executor() {
let composite = CompositeExecutor::new(FileToolExecutor, ShellToolExecutor);
let call = ToolCall {
tool_id: ToolName::new("bash"),
params: serde_json::Map::new(),
caller_id: None,
context: None,
tool_call_id: String::new(),
skill_name: None,
};
let result = composite.execute_tool_call(&call).await.unwrap().unwrap();
assert_eq!(result.summary, "shell_handler");
}
#[tokio::test]
async fn tool_call_unhandled_returns_none() {
let composite = CompositeExecutor::new(FileToolExecutor, ShellToolExecutor);
let call = ToolCall {
tool_id: ToolName::new("unknown"),
params: serde_json::Map::new(),
caller_id: None,
context: None,
tool_call_id: String::new(),
skill_name: None,
};
let result = composite.execute_tool_call(&call).await.unwrap();
assert!(result.is_none());
}
mod state_forwarding {
use super::*;
use crate::SkillTrustLevel;
use std::sync::Mutex;
#[derive(Debug, Default)]
struct SpyExecutor {
last_env: Mutex<Option<std::collections::HashMap<String, String>>>,
last_trust: Mutex<Option<SkillTrustLevel>>,
}
impl ToolExecutor for SpyExecutor {
async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
Ok(None)
}
fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
*self.last_env.lock().unwrap() = env;
}
fn set_effective_trust(&self, level: SkillTrustLevel) {
*self.last_trust.lock().unwrap() = Some(level);
}
crate::tool_executor_no_inner_defaults!();
}
#[derive(Debug)]
struct FixedConfirmation(bool);
impl ToolExecutor for FixedConfirmation {
async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
Ok(None)
}
fn requires_confirmation(&self, _call: &ToolCall) -> bool {
self.0
}
async fn execute_tool_call_confirmed(
&self,
call: &ToolCall,
) -> Result<Option<ToolOutput>, ToolError> {
self.execute_tool_call(call).await
}
fn checkpoint_undo(&self, _n: usize) -> crate::CheckpointActionResult {
crate::CheckpointActionResult::unsupported()
}
fn checkpoint_redo(&self) -> crate::CheckpointActionResult {
crate::CheckpointActionResult::unsupported()
}
fn checkpoint_list(&self) -> crate::CheckpointListResult {
crate::CheckpointListResult::default()
}
fn is_tool_speculatable(&self, _tool_id: &str) -> bool {
false
}
}
fn confirmation_call() -> ToolCall {
ToolCall {
tool_id: ToolName::new("shell"),
params: serde_json::Map::new(),
caller_id: None,
context: None,
tool_call_id: String::new(),
skill_name: None,
}
}
#[test]
fn requires_confirmation_false_when_both_leaves_false() {
let composite =
CompositeExecutor::new(FixedConfirmation(false), FixedConfirmation(false));
assert!(!composite.requires_confirmation(&confirmation_call()));
}
#[test]
fn requires_confirmation_true_when_first_leaf_true() {
let composite =
CompositeExecutor::new(FixedConfirmation(true), FixedConfirmation(false));
assert!(composite.requires_confirmation(&confirmation_call()));
}
#[test]
fn requires_confirmation_true_when_second_leaf_true() {
let composite =
CompositeExecutor::new(FixedConfirmation(false), FixedConfirmation(true));
assert!(composite.requires_confirmation(&confirmation_call()));
}
#[test]
fn requires_confirmation_or_forwards_across_nested_composition() {
let nested = CompositeExecutor::new(FixedConfirmation(false), FixedConfirmation(true));
let outer = CompositeExecutor::new(nested, FixedConfirmation(false));
assert!(
outer.requires_confirmation(&confirmation_call()),
"a confirmation requirement on a nested leaf must reach the outer composite"
);
}
#[test]
fn set_skill_env_reaches_both_inner_executors_in_nested_composition() {
let leaf_a = SpyExecutor::default();
let leaf_b = SpyExecutor::default();
let leaf_c = SpyExecutor::default();
let nested = CompositeExecutor::new(leaf_a, leaf_b);
let outer = CompositeExecutor::new(nested, leaf_c);
let mut env = std::collections::HashMap::new();
env.insert("GITHUB_TOKEN".to_owned(), "tok".to_owned());
outer.set_skill_env(Some(env.clone()));
assert_eq!(
outer.first.first.last_env.lock().unwrap().as_ref(),
Some(&env)
);
assert_eq!(
outer.first.second.last_env.lock().unwrap().as_ref(),
Some(&env)
);
assert_eq!(outer.second.last_env.lock().unwrap().as_ref(), Some(&env));
}
#[test]
fn set_effective_trust_reaches_both_inner_executors_in_nested_composition() {
let leaf_a = SpyExecutor::default();
let leaf_b = SpyExecutor::default();
let outer = CompositeExecutor::new(leaf_a, leaf_b);
outer.set_effective_trust(SkillTrustLevel::Quarantined);
assert_eq!(
*outer.first.last_trust.lock().unwrap(),
Some(SkillTrustLevel::Quarantined)
);
assert_eq!(
*outer.second.last_trust.lock().unwrap(),
Some(SkillTrustLevel::Quarantined)
);
}
}
mod optional_executor {
use super::*;
#[tokio::test]
async fn none_execute_returns_ok_none() {
let wrapped: OptionalExecutor<MatchingExecutor> = OptionalExecutor(None);
assert!(wrapped.execute("anything").await.unwrap().is_none());
}
#[tokio::test]
async fn some_execute_delegates_to_inner() {
let wrapped = OptionalExecutor(Some(MatchingExecutor));
let result = wrapped.execute("anything").await.unwrap();
assert_eq!(result.unwrap().summary, "matched");
}
#[tokio::test]
async fn none_execute_tool_call_returns_ok_none() {
let wrapped: OptionalExecutor<FileToolExecutor> = OptionalExecutor(None);
let call = ToolCall {
tool_id: ToolName::new("read"),
params: serde_json::Map::new(),
caller_id: None,
context: None,
tool_call_id: String::new(),
skill_name: None,
};
assert!(wrapped.execute_tool_call(&call).await.unwrap().is_none());
}
#[tokio::test]
async fn some_execute_tool_call_delegates_to_inner() {
let wrapped = OptionalExecutor(Some(FileToolExecutor));
let call = ToolCall {
tool_id: ToolName::new("read"),
params: serde_json::Map::new(),
caller_id: None,
context: None,
tool_call_id: String::new(),
skill_name: None,
};
let result = wrapped.execute_tool_call(&call).await.unwrap();
assert_eq!(result.unwrap().summary, "file_handler");
}
#[test]
fn none_tool_definitions_is_empty() {
let wrapped: OptionalExecutor<MatchingExecutor> = OptionalExecutor(None);
assert!(wrapped.tool_definitions().is_empty());
}
#[test]
fn none_checkpoint_undo_unsupported() {
let wrapped: OptionalExecutor<MatchingExecutor> = OptionalExecutor(None);
assert!(!wrapped.checkpoint_undo(1).supported);
assert!(!wrapped.checkpoint_redo().supported);
assert!(!wrapped.checkpoint_list().supported);
}
#[test]
fn none_not_retryable_or_speculatable() {
let wrapped: OptionalExecutor<MatchingExecutor> = OptionalExecutor(None);
assert!(!wrapped.is_tool_retryable("anything"));
assert!(!wrapped.is_tool_speculatable("anything"));
}
#[test]
fn none_requires_confirmation_false() {
let wrapped: OptionalExecutor<MatchingExecutor> = OptionalExecutor(None);
let call = ToolCall {
tool_id: ToolName::new("anything"),
params: serde_json::Map::new(),
caller_id: None,
context: None,
tool_call_id: String::new(),
skill_name: None,
};
assert!(!wrapped.requires_confirmation(&call));
}
}
}