use std::borrow::Cow;
use std::hash::Hasher;
use std::marker::PhantomData;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use anyhow::{Error, Result};
use async_trait::async_trait;
use serde_json::Value;
use crate::cache::{CacheKey, UnifiedCache};
use crate::tool_policy::ToolPolicy;
use crate::tools::handlers::tool_handler::{
ToolCallError, ToolHandler, ToolInvocation, ToolKind, ToolOutput, ToolPayload,
};
use crate::tools::result::ToolResult as SplitToolResult;
use crate::tools::traits::Tool;
pub trait HasComponent<Name> {
type Provider;
}
pub type ComponentProvider<Ctx, Name> = <Ctx as HasComponent<Name>>::Provider;
#[macro_export]
macro_rules! delegate_components {
($ctx:ty { $($name:ty => $provider:ty),* $(,)? }) => {
$(
impl $crate::components::HasComponent<$name> for $ctx {
type Provider = $provider;
}
)*
};
}
pub enum ApprovalComponent {}
pub enum SandboxComponent {}
pub enum ExecuteComponent {}
pub enum MetadataComponent {}
pub enum SessionComponent {}
pub enum OutputMapComponent {}
pub enum LoggingComponent {}
pub enum CacheComponent {}
pub enum RetryComponent {}
#[async_trait]
pub trait ApprovalProvider<Ctx: Send + Sync>: Send + Sync {
async fn check_approval(ctx: &Ctx, tool_name: &str, description: &str) -> Result<()>;
}
#[async_trait]
pub trait SandboxProvider<Ctx: Send + Sync>: Send + Sync {
fn sandbox_enabled(ctx: &Ctx) -> bool;
fn workspace_root(ctx: &Ctx) -> Option<&PathBuf>;
}
pub trait MetadataProvider<Ctx>: Send + Sync {
fn tool_name(_ctx: &Ctx) -> &'static str {
"unknown"
}
fn tool_description(_ctx: &Ctx) -> &'static str {
""
}
fn parameter_schema(_ctx: &Ctx) -> Option<Value> {
None
}
fn config_schema(_ctx: &Ctx) -> Option<Value> {
None
}
fn state_schema(_ctx: &Ctx) -> Option<Value> {
None
}
fn prompt_path(_ctx: &Ctx) -> Option<Cow<'static, str>> {
None
}
fn default_permission(_ctx: &Ctx) -> ToolPolicy {
ToolPolicy::Prompt
}
fn allow_patterns(_ctx: &Ctx) -> Option<&'static [&'static str]> {
None
}
fn deny_patterns(_ctx: &Ctx) -> Option<&'static [&'static str]> {
None
}
fn is_mutating(_ctx: &Ctx) -> bool {
true
}
fn is_parallel_safe(ctx: &Ctx) -> bool {
!Self::is_mutating(ctx)
}
fn tool_kind(_ctx: &Ctx) -> &'static str {
"unknown"
}
fn resource_hints(_ctx: &Ctx, _args: &Value) -> Vec<String> {
Vec::new()
}
fn execution_cost(_ctx: &Ctx) -> u8 {
5
}
}
pub trait OutputMapProvider<Ctx>: Send + Sync {
type Input;
type Output;
fn map_output(ctx: &Ctx, input: Self::Input) -> Self::Output;
}
#[async_trait]
pub trait ExecuteProvider<Ctx: Send + Sync>: Send + Sync {
async fn execute(ctx: &Ctx, args: Value) -> Result<Value>;
async fn execute_dual(ctx: &Ctx, args: Value) -> Result<SplitToolResult> {
let result = Self::execute(ctx, args).await?;
let name = if let Some(n) = result.get("tool_name").and_then(|v| v.as_str()) {
n.to_string()
} else {
"unknown".to_string()
};
let content = value_to_text(&result);
Ok(SplitToolResult::simple(&name, content))
}
}
pub trait LoggingProvider<Ctx>: Send + Sync {
fn on_start(_ctx: &Ctx, _tool_name: &str, _args: &Value) {}
fn on_cache_hit(_ctx: &Ctx, _tool_name: &str, _args: &Value) {}
fn on_success(
_ctx: &Ctx,
_tool_name: &str,
_duration: Duration,
_attempt: u32,
_from_cache: bool,
) {
}
fn on_retry(
_ctx: &Ctx,
_tool_name: &str,
_next_attempt: u32,
_backoff: Duration,
_error: &Error,
) {
}
fn on_failure(
_ctx: &Ctx,
_tool_name: &str,
_duration: Duration,
_attempt: u32,
_error: &Error,
) {
}
}
pub trait CacheProvider<Ctx>: Send + Sync {
fn get_json(_ctx: &Ctx, _tool_name: &str, _args: &Value) -> Option<Value> {
None
}
fn put_json(_ctx: &Ctx, _tool_name: &str, _args: &Value, _result: &Value) {}
fn get_dual(_ctx: &Ctx, _tool_name: &str, _args: &Value) -> Option<SplitToolResult> {
None
}
fn put_dual(_ctx: &Ctx, _tool_name: &str, _args: &Value, _result: &SplitToolResult) {}
}
pub trait RetryProvider<Ctx>: Send + Sync {
fn max_attempts(_ctx: &Ctx, _tool_name: &str, _args: &Value) -> u32 {
1
}
fn should_retry(_ctx: &Ctx, _tool_name: &str, _attempt: u32, _error: &Error) -> bool {
false
}
fn backoff_duration(_ctx: &Ctx, _tool_name: &str, _attempt: u32) -> Duration {
Duration::ZERO
}
}
pub struct AutoApproval;
#[async_trait]
impl<Ctx: Send + Sync> ApprovalProvider<Ctx> for AutoApproval {
async fn check_approval(_ctx: &Ctx, _tool_name: &str, _description: &str) -> Result<()> {
Ok(())
}
}
pub struct DenyAllApproval;
#[async_trait]
impl<Ctx: Send + Sync> ApprovalProvider<Ctx> for DenyAllApproval {
async fn check_approval(_ctx: &Ctx, tool_name: &str, _description: &str) -> Result<()> {
anyhow::bail!("operation denied: {tool_name} is not permitted in this context")
}
}
pub struct NoSandbox;
#[async_trait]
impl<Ctx: Send + Sync> SandboxProvider<Ctx> for NoSandbox {
fn sandbox_enabled(_ctx: &Ctx) -> bool {
false
}
fn workspace_root(_ctx: &Ctx) -> Option<&PathBuf> {
None
}
}
pub struct DefaultMetadata;
impl<Ctx> MetadataProvider<Ctx> for DefaultMetadata {}
pub struct NoLogging;
impl<Ctx> LoggingProvider<Ctx> for NoLogging {}
pub struct NoCache;
impl<Ctx> CacheProvider<Ctx> for NoCache {}
pub struct NoRetry;
impl<Ctx> RetryProvider<Ctx> for NoRetry {}
pub struct TracingLogging;
impl<Ctx> LoggingProvider<Ctx> for TracingLogging {
fn on_start(_ctx: &Ctx, tool_name: &str, _args: &Value) {
tracing::trace!(tool = %tool_name, "CGP tool execution started");
}
fn on_cache_hit(_ctx: &Ctx, tool_name: &str, _args: &Value) {
tracing::debug!(tool = %tool_name, "CGP tool result served from cache");
}
fn on_success(_ctx: &Ctx, tool_name: &str, duration: Duration, attempt: u32, from_cache: bool) {
tracing::trace!(
tool = %tool_name,
duration_ms = duration.as_millis() as u64,
attempt,
from_cache,
"CGP tool execution succeeded"
);
}
fn on_retry(_ctx: &Ctx, tool_name: &str, next_attempt: u32, backoff: Duration, error: &Error) {
tracing::debug!(
tool = %tool_name,
next_attempt,
backoff_ms = backoff.as_millis() as u64,
error = %error,
"CGP tool execution retry scheduled"
);
}
fn on_failure(_ctx: &Ctx, tool_name: &str, duration: Duration, attempt: u32, error: &Error) {
tracing::warn!(
tool = %tool_name,
duration_ms = duration.as_millis() as u64,
attempt,
error = %error,
"CGP tool execution failed"
);
}
}
#[derive(Debug, Clone, Copy)]
pub struct RetryPolicy {
pub max_attempts: u32,
pub initial_backoff: Duration,
pub max_backoff: Duration,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 3,
initial_backoff: Duration::from_millis(100),
max_backoff: Duration::from_secs(1),
}
}
}
pub trait HasRetryPolicy: Send + Sync {
fn retry_policy(&self) -> RetryPolicy;
}
pub struct ExponentialBackoffRetry;
impl<Ctx: HasRetryPolicy> RetryProvider<Ctx> for ExponentialBackoffRetry {
fn max_attempts(ctx: &Ctx, _tool_name: &str, _args: &Value) -> u32 {
ctx.retry_policy().max_attempts.max(1)
}
fn should_retry(_ctx: &Ctx, _tool_name: &str, _attempt: u32, _error: &Error) -> bool {
true
}
fn backoff_duration(ctx: &Ctx, _tool_name: &str, attempt: u32) -> Duration {
let policy = ctx.retry_policy();
let exponent = attempt.saturating_sub(1).min(31);
let factor = 2_u64.saturating_pow(exponent);
let millis = policy.initial_backoff.as_millis() as u64;
Duration::from_millis(millis.saturating_mul(factor)).min(policy.max_backoff)
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct ToolExecutionCacheKey(String);
impl CacheKey for ToolExecutionCacheKey {
fn to_cache_key(&self) -> String {
self.0.clone()
}
}
pub trait HasExecutionCaches: Send + Sync {
fn json_cache(&self) -> &UnifiedCache<ToolExecutionCacheKey, Value>;
fn dual_cache(&self) -> &UnifiedCache<ToolExecutionCacheKey, SplitToolResult>;
}
fn build_execution_cache_key(tool_name: &str, args: &Value) -> ToolExecutionCacheKey {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
hasher.write(tool_name.as_bytes());
hasher.write(
serde_json::to_string(args)
.unwrap_or_else(|_| args.to_string())
.as_bytes(),
);
ToolExecutionCacheKey(format!("{tool_name}::{}", hasher.finish()))
}
pub struct CachedResults;
impl<Ctx: HasExecutionCaches> CacheProvider<Ctx> for CachedResults {
fn get_json(ctx: &Ctx, tool_name: &str, args: &Value) -> Option<Value> {
ctx.json_cache()
.get_owned(&build_execution_cache_key(tool_name, args))
}
fn put_json(ctx: &Ctx, tool_name: &str, args: &Value, result: &Value) {
let key = build_execution_cache_key(tool_name, args);
let size = serde_json::to_string(result)
.map(|json| json.len() as u64)
.unwrap_or_default();
ctx.json_cache().insert(key, result.clone(), size);
}
fn get_dual(ctx: &Ctx, tool_name: &str, args: &Value) -> Option<SplitToolResult> {
ctx.dual_cache()
.get_owned(&build_execution_cache_key(tool_name, args))
}
fn put_dual(ctx: &Ctx, tool_name: &str, args: &Value, result: &SplitToolResult) {
let key = build_execution_cache_key(tool_name, args);
let size = (result.llm_content.len() + result.ui_content.len()) as u64;
ctx.dual_cache().insert(key, result.clone(), size);
}
}
pub struct PassthroughMetadata;
impl<Ctx: HasInnerTool> MetadataProvider<Ctx> for PassthroughMetadata {
fn tool_name(ctx: &Ctx) -> &'static str {
ctx.inner_tool().name()
}
fn tool_description(ctx: &Ctx) -> &'static str {
ctx.inner_tool().description()
}
fn parameter_schema(ctx: &Ctx) -> Option<Value> {
ctx.inner_tool().parameter_schema()
}
fn config_schema(ctx: &Ctx) -> Option<Value> {
ctx.inner_tool().config_schema()
}
fn state_schema(ctx: &Ctx) -> Option<Value> {
ctx.inner_tool().state_schema()
}
fn prompt_path(ctx: &Ctx) -> Option<Cow<'static, str>> {
ctx.inner_tool().prompt_path()
}
fn default_permission(ctx: &Ctx) -> ToolPolicy {
ctx.inner_tool().default_permission()
}
fn allow_patterns(ctx: &Ctx) -> Option<&'static [&'static str]> {
ctx.inner_tool().allow_patterns()
}
fn deny_patterns(ctx: &Ctx) -> Option<&'static [&'static str]> {
ctx.inner_tool().deny_patterns()
}
fn is_mutating(ctx: &Ctx) -> bool {
ctx.inner_tool().is_mutating()
}
fn is_parallel_safe(ctx: &Ctx) -> bool {
ctx.inner_tool().is_parallel_safe()
}
fn tool_kind(ctx: &Ctx) -> &'static str {
ctx.inner_tool().kind()
}
fn resource_hints(ctx: &Ctx, args: &Value) -> Vec<String> {
ctx.inner_tool().resource_hints(args)
}
fn execution_cost(ctx: &Ctx) -> u8 {
ctx.inner_tool().execution_cost()
}
}
fn value_to_text(value: &Value) -> String {
if value.is_string() {
value.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())
}
}
#[async_trait]
pub trait CanApproveTool: Send + Sync {
async fn approve_tool(&self, tool_name: &str, description: &str) -> Result<()>;
}
#[async_trait]
impl<Ctx> CanApproveTool for Ctx
where
Ctx: HasComponent<ApprovalComponent> + Send + Sync,
ComponentProvider<Ctx, ApprovalComponent>: ApprovalProvider<Ctx>,
{
async fn approve_tool(&self, tool_name: &str, description: &str) -> Result<()> {
<ComponentProvider<Ctx, ApprovalComponent> as ApprovalProvider<Ctx>>::check_approval(
self,
tool_name,
description,
)
.await
}
}
pub trait CanResolveSandbox: Send + Sync {
fn sandbox_enabled(&self) -> bool;
fn workspace_root(&self) -> Option<&PathBuf>;
}
impl<Ctx> CanResolveSandbox for Ctx
where
Ctx: HasComponent<SandboxComponent> + Send + Sync,
ComponentProvider<Ctx, SandboxComponent>: SandboxProvider<Ctx>,
{
fn sandbox_enabled(&self) -> bool {
<ComponentProvider<Ctx, SandboxComponent> as SandboxProvider<Ctx>>::sandbox_enabled(self)
}
fn workspace_root(&self) -> Option<&PathBuf> {
<ComponentProvider<Ctx, SandboxComponent> as SandboxProvider<Ctx>>::workspace_root(self)
}
}
pub trait CanProvideToolMetadata: Send + Sync {
fn tool_name(&self) -> &'static str;
fn tool_description(&self) -> &'static str;
fn parameter_schema(&self) -> Option<Value>;
fn config_schema(&self) -> Option<Value>;
fn state_schema(&self) -> Option<Value>;
fn prompt_path(&self) -> Option<Cow<'static, str>>;
fn default_permission(&self) -> ToolPolicy;
fn allow_patterns(&self) -> Option<&'static [&'static str]>;
fn deny_patterns(&self) -> Option<&'static [&'static str]>;
fn is_mutating(&self) -> bool;
fn is_parallel_safe(&self) -> bool;
fn tool_kind(&self) -> &'static str;
fn resource_hints(&self, args: &Value) -> Vec<String>;
fn execution_cost(&self) -> u8;
}
impl<Ctx> CanProvideToolMetadata for Ctx
where
Ctx: HasComponent<MetadataComponent> + Send + Sync,
ComponentProvider<Ctx, MetadataComponent>: MetadataProvider<Ctx>,
{
fn tool_name(&self) -> &'static str {
<ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::tool_name(self)
}
fn tool_description(&self) -> &'static str {
<ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::tool_description(self)
}
fn parameter_schema(&self) -> Option<Value> {
<ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::parameter_schema(self)
}
fn config_schema(&self) -> Option<Value> {
<ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::config_schema(self)
}
fn state_schema(&self) -> Option<Value> {
<ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::state_schema(self)
}
fn prompt_path(&self) -> Option<Cow<'static, str>> {
<ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::prompt_path(self)
}
fn default_permission(&self) -> ToolPolicy {
<ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::default_permission(
self,
)
}
fn allow_patterns(&self) -> Option<&'static [&'static str]> {
<ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::allow_patterns(self)
}
fn deny_patterns(&self) -> Option<&'static [&'static str]> {
<ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::deny_patterns(self)
}
fn is_mutating(&self) -> bool {
<ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::is_mutating(self)
}
fn is_parallel_safe(&self) -> bool {
<ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::is_parallel_safe(self)
}
fn tool_kind(&self) -> &'static str {
<ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::tool_kind(self)
}
fn resource_hints(&self, args: &Value) -> Vec<String> {
<ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::resource_hints(
self, args,
)
}
fn execution_cost(&self) -> u8 {
<ComponentProvider<Ctx, MetadataComponent> as MetadataProvider<Ctx>>::execution_cost(self)
}
}
#[async_trait]
pub trait CanExecuteTool: Send + Sync {
async fn execute_tool_json(&self, tool_name: &str, args: Value) -> Result<Value>;
async fn execute_tool_dual(&self, tool_name: &str, args: Value) -> Result<SplitToolResult>;
}
#[async_trait]
impl<Ctx> CanExecuteTool for Ctx
where
Ctx: HasComponent<ExecuteComponent>
+ HasComponent<LoggingComponent>
+ HasComponent<CacheComponent>
+ HasComponent<RetryComponent>
+ Send
+ Sync,
ComponentProvider<Ctx, ExecuteComponent>: ExecuteProvider<Ctx>,
ComponentProvider<Ctx, LoggingComponent>: LoggingProvider<Ctx>,
ComponentProvider<Ctx, CacheComponent>: CacheProvider<Ctx>,
ComponentProvider<Ctx, RetryComponent>: RetryProvider<Ctx>,
{
async fn execute_tool_json(&self, tool_name: &str, args: Value) -> Result<Value> {
<ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_start(
self, tool_name, &args,
);
if let Some(result) =
<ComponentProvider<Ctx, CacheComponent> as CacheProvider<Ctx>>::get_json(
self, tool_name, &args,
)
{
<ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_cache_hit(
self, tool_name, &args,
);
<ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_success(
self,
tool_name,
Duration::ZERO,
1,
true,
);
return Ok(result);
}
let started = Instant::now();
let max_attempts =
<ComponentProvider<Ctx, RetryComponent> as RetryProvider<Ctx>>::max_attempts(
self, tool_name, &args,
)
.max(1);
for attempt_index in 0..max_attempts {
let attempt = attempt_index + 1;
match <ComponentProvider<Ctx, ExecuteComponent> as ExecuteProvider<Ctx>>::execute(
self,
args.clone(),
)
.await
{
Ok(result) => {
<ComponentProvider<Ctx, CacheComponent> as CacheProvider<Ctx>>::put_json(
self, tool_name, &args, &result,
);
<ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_success(
self,
tool_name,
started.elapsed(),
attempt,
false,
);
return Ok(result);
}
Err(error) => {
let should_retry = attempt < max_attempts
&& <ComponentProvider<Ctx, RetryComponent> as RetryProvider<Ctx>>::should_retry(
self, tool_name, attempt, &error,
);
if should_retry {
let backoff = <ComponentProvider<Ctx, RetryComponent> as RetryProvider<
Ctx,
>>::backoff_duration(
self, tool_name, attempt
);
<ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_retry(
self,
tool_name,
attempt + 1,
backoff,
&error,
);
if !backoff.is_zero() {
tokio::time::sleep(backoff).await;
}
continue;
}
<ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_failure(
self,
tool_name,
started.elapsed(),
attempt,
&error,
);
return Err(error);
}
}
}
unreachable!("retry loop always returns or continues")
}
async fn execute_tool_dual(&self, tool_name: &str, args: Value) -> Result<SplitToolResult> {
<ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_start(
self, tool_name, &args,
);
if let Some(result) =
<ComponentProvider<Ctx, CacheComponent> as CacheProvider<Ctx>>::get_dual(
self, tool_name, &args,
)
{
<ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_cache_hit(
self, tool_name, &args,
);
<ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_success(
self,
tool_name,
Duration::ZERO,
1,
true,
);
return Ok(result);
}
let started = Instant::now();
let max_attempts =
<ComponentProvider<Ctx, RetryComponent> as RetryProvider<Ctx>>::max_attempts(
self, tool_name, &args,
)
.max(1);
for attempt_index in 0..max_attempts {
let attempt = attempt_index + 1;
match <ComponentProvider<Ctx, ExecuteComponent> as ExecuteProvider<Ctx>>::execute_dual(
self,
args.clone(),
)
.await
{
Ok(result) => {
<ComponentProvider<Ctx, CacheComponent> as CacheProvider<Ctx>>::put_dual(
self, tool_name, &args, &result,
);
<ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_success(
self,
tool_name,
started.elapsed(),
attempt,
false,
);
return Ok(result);
}
Err(error) => {
let should_retry = attempt < max_attempts
&& <ComponentProvider<Ctx, RetryComponent> as RetryProvider<Ctx>>::should_retry(
self, tool_name, attempt, &error,
);
if should_retry {
let backoff = <ComponentProvider<Ctx, RetryComponent> as RetryProvider<
Ctx,
>>::backoff_duration(
self, tool_name, attempt
);
<ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_retry(
self,
tool_name,
attempt + 1,
backoff,
&error,
);
if !backoff.is_zero() {
tokio::time::sleep(backoff).await;
}
continue;
}
<ComponentProvider<Ctx, LoggingComponent> as LoggingProvider<Ctx>>::on_failure(
self,
tool_name,
started.elapsed(),
attempt,
&error,
);
return Err(error);
}
}
}
unreachable!("retry loop always returns or continues")
}
}
pub struct ToolFacade<Ctx> {
ctx: Ctx,
}
impl<Ctx> ToolFacade<Ctx> {
pub fn new(ctx: Ctx) -> Self {
Self { ctx }
}
}
#[async_trait]
impl<Ctx> Tool for ToolFacade<Ctx>
where
Ctx: CanApproveTool + CanExecuteTool + CanProvideToolMetadata + Send + Sync + 'static,
{
async fn execute(&self, args: Value) -> Result<Value> {
self.ctx.approve_tool(self.name(), "execute").await?;
self.ctx.execute_tool_json(self.name(), args).await
}
async fn execute_dual(&self, args: Value) -> Result<SplitToolResult> {
self.ctx.approve_tool(self.name(), "execute_dual").await?;
self.ctx.execute_tool_dual(self.name(), args).await
}
fn name(&self) -> &'static str {
CanProvideToolMetadata::tool_name(&self.ctx)
}
fn description(&self) -> &'static str {
CanProvideToolMetadata::tool_description(&self.ctx)
}
fn parameter_schema(&self) -> Option<Value> {
CanProvideToolMetadata::parameter_schema(&self.ctx)
}
fn config_schema(&self) -> Option<Value> {
CanProvideToolMetadata::config_schema(&self.ctx)
}
fn state_schema(&self) -> Option<Value> {
CanProvideToolMetadata::state_schema(&self.ctx)
}
fn prompt_path(&self) -> Option<Cow<'static, str>> {
CanProvideToolMetadata::prompt_path(&self.ctx)
}
fn default_permission(&self) -> ToolPolicy {
CanProvideToolMetadata::default_permission(&self.ctx)
}
fn allow_patterns(&self) -> Option<&'static [&'static str]> {
CanProvideToolMetadata::allow_patterns(&self.ctx)
}
fn deny_patterns(&self) -> Option<&'static [&'static str]> {
CanProvideToolMetadata::deny_patterns(&self.ctx)
}
fn is_mutating(&self) -> bool {
CanProvideToolMetadata::is_mutating(&self.ctx)
}
fn is_parallel_safe(&self) -> bool {
CanProvideToolMetadata::is_parallel_safe(&self.ctx)
}
fn kind(&self) -> &'static str {
CanProvideToolMetadata::tool_kind(&self.ctx)
}
fn resource_hints(&self, args: &Value) -> Vec<String> {
CanProvideToolMetadata::resource_hints(&self.ctx, args)
}
fn execution_cost(&self) -> u8 {
CanProvideToolMetadata::execution_cost(&self.ctx)
}
}
pub struct HandlerFacade<Ctx> {
ctx: Ctx,
}
impl<Ctx> HandlerFacade<Ctx> {
pub fn new(ctx: Ctx) -> Self {
Self { ctx }
}
}
#[async_trait]
impl<Ctx> ToolHandler for HandlerFacade<Ctx>
where
Ctx: CanApproveTool + CanExecuteTool + Send + Sync + 'static,
{
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, ToolCallError> {
let args: Value = match &invocation.payload {
ToolPayload::Function { arguments } => serde_json::from_str(arguments)
.map_err(|e| ToolCallError::respond(format!("Invalid arguments: {e}")))?,
_ => return Err(ToolCallError::respond("Unsupported payload type")),
};
self.ctx
.approve_tool(&invocation.tool_name, "handle")
.await
.map_err(|e| ToolCallError::respond(e.to_string()))?;
match self
.ctx
.execute_tool_json(&invocation.tool_name, args)
.await
{
Ok(result) => {
let text = value_to_text(&result);
Ok(ToolOutput::simple(text))
}
Err(e) => Err(ToolCallError::Internal(e)),
}
}
}
pub struct ComposableRuntime;
impl ComposableRuntime {
pub async fn run<Ctx>(ctx: &Ctx, tool_name: &str, description: &str) -> Result<()>
where
Ctx: CanApproveTool + Send + Sync,
{
ctx.approve_tool(tool_name, description).await?;
Ok(())
}
pub async fn run_with_sandbox<Ctx>(
ctx: &Ctx,
tool_name: &str,
description: &str,
) -> Result<bool>
where
Ctx: CanApproveTool + CanResolveSandbox + Send + Sync,
{
ctx.approve_tool(tool_name, description).await?;
Ok(ctx.sandbox_enabled())
}
}
pub struct InteractiveCtx {
pub workspace_root: PathBuf,
}
impl InteractiveCtx {
pub fn new(workspace_root: PathBuf) -> Self {
Self { workspace_root }
}
}
pub struct PromptApproval;
#[async_trait]
impl<Ctx: Send + Sync> ApprovalProvider<Ctx> for PromptApproval {
async fn check_approval(_ctx: &Ctx, _tool_name: &str, _description: &str) -> Result<()> {
Ok(())
}
}
pub trait HasWorkspaceRoot: Send + Sync {
fn workspace_root(&self) -> &PathBuf;
}
impl HasWorkspaceRoot for InteractiveCtx {
fn workspace_root(&self) -> &PathBuf {
&self.workspace_root
}
}
pub struct WorkspaceSandbox;
#[async_trait]
impl<Ctx: HasWorkspaceRoot> SandboxProvider<Ctx> for WorkspaceSandbox {
fn sandbox_enabled(_ctx: &Ctx) -> bool {
true
}
fn workspace_root(ctx: &Ctx) -> Option<&PathBuf> {
Some(HasWorkspaceRoot::workspace_root(ctx))
}
}
delegate_components!(InteractiveCtx {
ApprovalComponent => PromptApproval,
SandboxComponent => WorkspaceSandbox,
ExecuteComponent => PassthroughExecutor,
MetadataComponent => PassthroughMetadata,
LoggingComponent => TracingLogging,
CacheComponent => NoCache,
RetryComponent => NoRetry,
});
pub struct CiCtx {
pub workspace_root: PathBuf,
}
impl CiCtx {
pub fn new(workspace_root: PathBuf) -> Self {
Self { workspace_root }
}
}
impl HasWorkspaceRoot for CiCtx {
fn workspace_root(&self) -> &PathBuf {
&self.workspace_root
}
}
pub struct StrictWorkspaceSandbox;
#[async_trait]
impl<Ctx: HasWorkspaceRoot> SandboxProvider<Ctx> for StrictWorkspaceSandbox {
fn sandbox_enabled(_ctx: &Ctx) -> bool {
true
}
fn workspace_root(ctx: &Ctx) -> Option<&PathBuf> {
Some(HasWorkspaceRoot::workspace_root(ctx))
}
}
delegate_components!(CiCtx {
ApprovalComponent => AutoApproval,
SandboxComponent => StrictWorkspaceSandbox,
ExecuteComponent => PassthroughExecutor,
MetadataComponent => PassthroughMetadata,
LoggingComponent => NoLogging,
CacheComponent => NoCache,
RetryComponent => NoRetry,
});
pub struct BenchCtx;
delegate_components!(BenchCtx {
ApprovalComponent => AutoApproval,
SandboxComponent => NoSandbox,
ExecuteComponent => PassthroughExecutor,
MetadataComponent => PassthroughMetadata,
LoggingComponent => NoLogging,
CacheComponent => NoCache,
RetryComponent => NoRetry,
});
pub struct PassthroughExecutor;
pub trait HasInnerTool: Send + Sync {
fn inner_tool(&self) -> &Arc<dyn Tool>;
}
#[async_trait]
impl<Ctx: HasInnerTool> ExecuteProvider<Ctx> for PassthroughExecutor {
async fn execute(ctx: &Ctx, args: Value) -> Result<Value> {
ctx.inner_tool().execute(args).await
}
async fn execute_dual(ctx: &Ctx, args: Value) -> Result<SplitToolResult> {
ctx.inner_tool().execute_dual(args).await
}
}
pub fn wrap_tool_interactive(
tool: Arc<dyn Tool>,
workspace_root: PathBuf,
) -> ToolFacade<ToolBridgeCtx<InteractiveCtx>> {
let ctx = ToolBridgeCtx {
inner: tool,
runtime: InteractiveCtx::new(workspace_root),
};
ToolFacade::new(ctx)
}
pub fn wrap_tool_ci(
tool: Arc<dyn Tool>,
workspace_root: PathBuf,
) -> ToolFacade<ToolBridgeCtx<CiCtx>> {
let ctx = ToolBridgeCtx {
inner: tool,
runtime: CiCtx::new(workspace_root),
};
ToolFacade::new(ctx)
}
pub struct ToolBridgeCtx<Runtime> {
inner: Arc<dyn Tool>,
#[allow(dead_code)]
runtime: Runtime,
}
impl<Runtime: HasWorkspaceRoot> HasWorkspaceRoot for ToolBridgeCtx<Runtime> {
fn workspace_root(&self) -> &PathBuf {
self.runtime.workspace_root()
}
}
impl<Runtime: Send + Sync> HasInnerTool for ToolBridgeCtx<Runtime> {
fn inner_tool(&self) -> &Arc<dyn Tool> {
&self.inner
}
}
impl<Name, Runtime> HasComponent<Name> for ToolBridgeCtx<Runtime>
where
Runtime: HasComponent<Name>,
{
type Provider = ComponentProvider<Runtime, Name>;
}
pub trait HasToolInstance<T>: Send + Sync {
fn tool_instance(&self) -> &T;
}
pub struct TypedToolExecutor<T>(PhantomData<T>);
#[async_trait]
impl<Ctx, T> ExecuteProvider<Ctx> for TypedToolExecutor<T>
where
Ctx: HasToolInstance<T> + Send + Sync,
T: Tool + Send + Sync,
{
async fn execute(ctx: &Ctx, args: Value) -> Result<Value> {
ctx.tool_instance().execute(args).await
}
async fn execute_dual(ctx: &Ctx, args: Value) -> Result<SplitToolResult> {
ctx.tool_instance().execute_dual(args).await
}
}
pub struct TypedToolMetadata<T>(PhantomData<T>);
impl<Ctx, T> MetadataProvider<Ctx> for TypedToolMetadata<T>
where
Ctx: HasToolInstance<T>,
T: Tool + Send + Sync,
{
fn tool_name(ctx: &Ctx) -> &'static str {
ctx.tool_instance().name()
}
fn tool_description(ctx: &Ctx) -> &'static str {
ctx.tool_instance().description()
}
fn parameter_schema(ctx: &Ctx) -> Option<Value> {
ctx.tool_instance().parameter_schema()
}
fn config_schema(ctx: &Ctx) -> Option<Value> {
ctx.tool_instance().config_schema()
}
fn state_schema(ctx: &Ctx) -> Option<Value> {
ctx.tool_instance().state_schema()
}
fn prompt_path(ctx: &Ctx) -> Option<Cow<'static, str>> {
ctx.tool_instance().prompt_path()
}
fn default_permission(ctx: &Ctx) -> ToolPolicy {
ctx.tool_instance().default_permission()
}
fn allow_patterns(ctx: &Ctx) -> Option<&'static [&'static str]> {
ctx.tool_instance().allow_patterns()
}
fn deny_patterns(ctx: &Ctx) -> Option<&'static [&'static str]> {
ctx.tool_instance().deny_patterns()
}
fn is_mutating(ctx: &Ctx) -> bool {
ctx.tool_instance().is_mutating()
}
fn is_parallel_safe(ctx: &Ctx) -> bool {
ctx.tool_instance().is_parallel_safe()
}
fn tool_kind(ctx: &Ctx) -> &'static str {
ctx.tool_instance().kind()
}
fn resource_hints(ctx: &Ctx, args: &Value) -> Vec<String> {
ctx.tool_instance().resource_hints(args)
}
fn execution_cost(ctx: &Ctx) -> u8 {
ctx.tool_instance().execution_cost()
}
}
pub struct TypedToolCtx<Runtime, T> {
tool: T,
runtime: Runtime,
}
impl<Runtime, T> TypedToolCtx<Runtime, T> {
pub fn new(tool: T, runtime: Runtime) -> Self {
Self { tool, runtime }
}
}
impl<Runtime: HasWorkspaceRoot + Send + Sync, T: Send + Sync> HasWorkspaceRoot
for TypedToolCtx<Runtime, T>
{
fn workspace_root(&self) -> &PathBuf {
self.runtime.workspace_root()
}
}
impl<Runtime: Send + Sync, T: Send + Sync> HasToolInstance<T> for TypedToolCtx<Runtime, T> {
fn tool_instance(&self) -> &T {
&self.tool
}
}
impl<Runtime, T> HasComponent<ApprovalComponent> for TypedToolCtx<Runtime, T>
where
Runtime: HasComponent<ApprovalComponent>,
{
type Provider = ComponentProvider<Runtime, ApprovalComponent>;
}
impl<Runtime, T> HasComponent<SandboxComponent> for TypedToolCtx<Runtime, T>
where
Runtime: HasComponent<SandboxComponent>,
{
type Provider = ComponentProvider<Runtime, SandboxComponent>;
}
impl<Runtime, T> HasComponent<SessionComponent> for TypedToolCtx<Runtime, T>
where
Runtime: HasComponent<SessionComponent>,
{
type Provider = ComponentProvider<Runtime, SessionComponent>;
}
impl<Runtime, T> HasComponent<OutputMapComponent> for TypedToolCtx<Runtime, T>
where
Runtime: HasComponent<OutputMapComponent>,
{
type Provider = ComponentProvider<Runtime, OutputMapComponent>;
}
impl<Runtime, T> HasComponent<LoggingComponent> for TypedToolCtx<Runtime, T>
where
Runtime: HasComponent<LoggingComponent>,
{
type Provider = ComponentProvider<Runtime, LoggingComponent>;
}
impl<Runtime, T> HasComponent<CacheComponent> for TypedToolCtx<Runtime, T>
where
Runtime: HasComponent<CacheComponent>,
{
type Provider = ComponentProvider<Runtime, CacheComponent>;
}
impl<Runtime, T> HasComponent<RetryComponent> for TypedToolCtx<Runtime, T>
where
Runtime: HasComponent<RetryComponent>,
{
type Provider = ComponentProvider<Runtime, RetryComponent>;
}
impl<Runtime, T> HasComponent<ExecuteComponent> for TypedToolCtx<Runtime, T>
where
Runtime: Send + Sync,
T: Tool + Send + Sync,
{
type Provider = TypedToolExecutor<T>;
}
impl<Runtime, T> HasComponent<MetadataComponent> for TypedToolCtx<Runtime, T>
where
Runtime: Send + Sync,
T: Tool + Send + Sync,
{
type Provider = TypedToolMetadata<T>;
}
pub fn wrap_native_tool_interactive<T>(
tool: T,
workspace_root: PathBuf,
) -> ToolFacade<TypedToolCtx<InteractiveCtx, T>>
where
T: Tool + Send + Sync + 'static,
{
ToolFacade::new(TypedToolCtx::new(tool, InteractiveCtx::new(workspace_root)))
}
pub fn wrap_native_tool_ci<T>(
tool: T,
workspace_root: PathBuf,
) -> ToolFacade<TypedToolCtx<CiCtx, T>>
where
T: Tool + Send + Sync + 'static,
{
ToolFacade::new(TypedToolCtx::new(tool, CiCtx::new(workspace_root)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cache::EvictionPolicy;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
struct EchoExecutor;
#[async_trait]
impl<Ctx: Send + Sync> ExecuteProvider<Ctx> for EchoExecutor {
async fn execute(_ctx: &Ctx, args: Value) -> Result<Value> {
Ok(serde_json::json!({
"tool_name": "echo",
"echoed": args,
}))
}
}
struct TestAutoCtx;
struct EchoMetadata;
impl<Ctx> MetadataProvider<Ctx> for EchoMetadata {
fn tool_name(_ctx: &Ctx) -> &'static str {
"echo"
}
fn tool_description(_ctx: &Ctx) -> &'static str {
"Echo tool"
}
}
delegate_components!(TestAutoCtx {
ApprovalComponent => AutoApproval,
SandboxComponent => NoSandbox,
ExecuteComponent => EchoExecutor,
MetadataComponent => EchoMetadata,
LoggingComponent => NoLogging,
CacheComponent => NoCache,
RetryComponent => NoRetry,
});
struct TestDenyCtx;
struct ExecMetadata;
impl<Ctx> MetadataProvider<Ctx> for ExecMetadata {
fn tool_name(_ctx: &Ctx) -> &'static str {
"exec"
}
fn tool_description(_ctx: &Ctx) -> &'static str {
"Exec tool"
}
}
delegate_components!(TestDenyCtx {
ApprovalComponent => DenyAllApproval,
SandboxComponent => NoSandbox,
ExecuteComponent => EchoExecutor,
MetadataComponent => ExecMetadata,
LoggingComponent => NoLogging,
CacheComponent => NoCache,
RetryComponent => NoRetry,
});
struct TestTracingCtx;
struct TracedToolMetadata;
impl<Ctx> MetadataProvider<Ctx> for TracedToolMetadata {
fn tool_name(_ctx: &Ctx) -> &'static str {
"traced_tool"
}
fn tool_description(_ctx: &Ctx) -> &'static str {
"A traced tool"
}
}
delegate_components!(TestTracingCtx {
ApprovalComponent => AutoApproval,
SandboxComponent => NoSandbox,
ExecuteComponent => EchoExecutor,
MetadataComponent => TracedToolMetadata,
LoggingComponent => TracingLogging,
CacheComponent => NoCache,
RetryComponent => NoRetry,
});
struct NamedToolCtx;
struct NamedToolMetadata;
impl<Ctx> MetadataProvider<Ctx> for NamedToolMetadata {
fn tool_name(_ctx: &Ctx) -> &'static str {
"my_tool"
}
fn tool_description(_ctx: &Ctx) -> &'static str {
"My description"
}
}
delegate_components!(NamedToolCtx {
ApprovalComponent => AutoApproval,
SandboxComponent => NoSandbox,
ExecuteComponent => EchoExecutor,
MetadataComponent => NamedToolMetadata,
LoggingComponent => NoLogging,
CacheComponent => NoCache,
RetryComponent => NoRetry,
});
trait HasExecutionCount: Send + Sync {
fn execution_count(&self) -> &AtomicUsize;
}
struct CountingExecutor;
#[async_trait]
impl<Ctx: HasExecutionCount + Send + Sync> ExecuteProvider<Ctx> for CountingExecutor {
async fn execute(ctx: &Ctx, args: Value) -> Result<Value> {
let count = ctx.execution_count().fetch_add(1, Ordering::SeqCst) + 1;
Ok(serde_json::json!({
"tool_name": "counting",
"attempt": count,
"args": args,
}))
}
}
struct TestCachingCtx {
executions: Arc<AtomicUsize>,
json_cache: UnifiedCache<ToolExecutionCacheKey, Value>,
dual_cache: UnifiedCache<ToolExecutionCacheKey, SplitToolResult>,
}
impl TestCachingCtx {
fn new(executions: Arc<AtomicUsize>) -> Self {
Self {
executions,
json_cache: UnifiedCache::new(8, Duration::from_secs(60), EvictionPolicy::Lru),
dual_cache: UnifiedCache::new(8, Duration::from_secs(60), EvictionPolicy::Lru),
}
}
}
impl HasExecutionCount for TestCachingCtx {
fn execution_count(&self) -> &AtomicUsize {
self.executions.as_ref()
}
}
impl HasExecutionCaches for TestCachingCtx {
fn json_cache(&self) -> &UnifiedCache<ToolExecutionCacheKey, Value> {
&self.json_cache
}
fn dual_cache(&self) -> &UnifiedCache<ToolExecutionCacheKey, SplitToolResult> {
&self.dual_cache
}
}
struct CachedToolMetadata;
impl<Ctx> MetadataProvider<Ctx> for CachedToolMetadata {
fn tool_name(_ctx: &Ctx) -> &'static str {
"cached_tool"
}
fn tool_description(_ctx: &Ctx) -> &'static str {
"A cached tool"
}
}
delegate_components!(TestCachingCtx {
ApprovalComponent => AutoApproval,
SandboxComponent => NoSandbox,
ExecuteComponent => CountingExecutor,
MetadataComponent => CachedToolMetadata,
LoggingComponent => NoLogging,
CacheComponent => CachedResults,
RetryComponent => NoRetry,
});
struct FlakyExecutor;
#[async_trait]
impl<Ctx: HasExecutionCount + Send + Sync> ExecuteProvider<Ctx> for FlakyExecutor {
async fn execute(ctx: &Ctx, args: Value) -> Result<Value> {
let attempt = ctx.execution_count().fetch_add(1, Ordering::SeqCst) + 1;
if attempt == 1 {
anyhow::bail!("transient failure")
}
Ok(serde_json::json!({
"tool_name": "flaky",
"attempt": attempt,
"args": args,
}))
}
}
struct TestRetryCtx {
executions: Arc<AtomicUsize>,
retry_policy: RetryPolicy,
}
impl TestRetryCtx {
fn new(executions: Arc<AtomicUsize>, retry_policy: RetryPolicy) -> Self {
Self {
executions,
retry_policy,
}
}
}
impl HasExecutionCount for TestRetryCtx {
fn execution_count(&self) -> &AtomicUsize {
self.executions.as_ref()
}
}
impl HasRetryPolicy for TestRetryCtx {
fn retry_policy(&self) -> RetryPolicy {
self.retry_policy
}
}
struct FlakyToolMetadata;
impl<Ctx> MetadataProvider<Ctx> for FlakyToolMetadata {
fn tool_name(_ctx: &Ctx) -> &'static str {
"flaky_tool"
}
fn tool_description(_ctx: &Ctx) -> &'static str {
"A flaky tool"
}
}
delegate_components!(TestRetryCtx {
ApprovalComponent => AutoApproval,
SandboxComponent => NoSandbox,
ExecuteComponent => FlakyExecutor,
MetadataComponent => FlakyToolMetadata,
LoggingComponent => NoLogging,
CacheComponent => NoCache,
RetryComponent => ExponentialBackoffRetry,
});
#[tokio::test]
async fn auto_ctx_approves() {
let ctx = TestAutoCtx;
let result = ComposableRuntime::run(&ctx, "grep", "search files").await;
assert!(result.is_ok());
}
#[tokio::test]
async fn consumer_trait_executes_directly_on_context() {
let ctx = TestAutoCtx;
let result = ctx
.execute_tool_json("echo", serde_json::json!({"via": "consumer"}))
.await
.expect("context capability should execute");
assert_eq!(
result
.get("echoed")
.and_then(|value| value.get("via"))
.and_then(|value| value.as_str()),
Some("consumer")
);
}
#[tokio::test]
async fn deny_ctx_rejects() {
let ctx = TestDenyCtx;
let result = ComposableRuntime::run(&ctx, "exec", "run command").await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("operation denied"));
}
#[tokio::test]
async fn sandbox_check_returns_policy() {
let ctx = TestAutoCtx;
let sandboxed = ComposableRuntime::run_with_sandbox(&ctx, "file_write", "write file")
.await
.expect("should succeed");
assert!(!sandboxed); }
struct StrictSandbox;
#[async_trait]
impl<Ctx: Send + Sync> SandboxProvider<Ctx> for StrictSandbox {
fn sandbox_enabled(_ctx: &Ctx) -> bool {
true
}
fn workspace_root(_ctx: &Ctx) -> Option<&PathBuf> {
None
}
}
struct StrictCtx;
delegate_components!(StrictCtx {
ApprovalComponent => AutoApproval,
SandboxComponent => StrictSandbox,
});
#[tokio::test]
async fn strict_ctx_enables_sandbox() {
let ctx = StrictCtx;
let sandboxed = ComposableRuntime::run_with_sandbox(&ctx, "exec", "run cmd")
.await
.expect("should succeed");
assert!(sandboxed);
}
#[tokio::test]
async fn tool_facade_executes_via_cgp() {
let facade = ToolFacade::new(TestAutoCtx);
let result = facade
.execute(serde_json::json!({"msg": "hello"}))
.await
.expect("should succeed");
assert_eq!(
result
.get("echoed")
.and_then(|v| v.get("msg"))
.and_then(|v| v.as_str()),
Some("hello")
);
}
#[tokio::test]
async fn tool_facade_denied_by_ctx() {
let facade = ToolFacade::new(TestDenyCtx);
let result = facade.execute(serde_json::json!({})).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("operation denied"));
}
#[tokio::test]
async fn tool_facade_name_and_description() {
let facade = ToolFacade::new(NamedToolCtx);
assert_eq!(facade.name(), "my_tool");
assert_eq!(facade.description(), "My description");
}
#[tokio::test]
async fn tool_facade_dual_output() {
let facade = ToolFacade::new(TestAutoCtx);
let result = facade
.execute_dual(serde_json::json!({"key": "value"}))
.await
.expect("should succeed");
assert!(result.success);
}
#[tokio::test]
async fn handler_facade_executes_via_cgp() {
let facade = HandlerFacade::new(TestAutoCtx);
let session: Arc<dyn crate::tools::handlers::tool_handler::ToolSession> = Arc::new(
crate::tools::handlers::adapter::DefaultToolSession::new(PathBuf::from("/tmp")),
);
let turn = Arc::new(crate::tools::handlers::tool_handler::TurnContext {
cwd: PathBuf::from("/tmp"),
turn_id: "test".to_string(),
sub_id: None,
shell_environment_policy:
crate::tools::handlers::tool_handler::ShellEnvironmentPolicy::default(),
approval_policy: crate::tools::handlers::tool_handler::Constrained::allow_any(
crate::tools::handlers::tool_handler::ApprovalPolicy::default(),
),
codex_linux_sandbox_exe: None,
sandbox_policy: crate::tools::handlers::tool_handler::Constrained::allow_any(
Default::default(),
),
});
let invocation = ToolInvocation {
session,
turn,
tracker: None,
call_id: "test-call".to_string(),
tool_name: "echo".to_string(),
payload: ToolPayload::Function {
arguments: r#"{"msg":"handler"}"#.to_string(),
},
};
let output = facade.handle(invocation).await.expect("should succeed");
assert!(output.is_success());
let content = output.content().expect("should have content");
assert!(content.contains("handler"));
}
#[tokio::test]
async fn handler_facade_denied_by_ctx() {
let facade = HandlerFacade::new(TestDenyCtx);
let session: Arc<dyn crate::tools::handlers::tool_handler::ToolSession> = Arc::new(
crate::tools::handlers::adapter::DefaultToolSession::new(PathBuf::from("/tmp")),
);
let turn = Arc::new(crate::tools::handlers::tool_handler::TurnContext {
cwd: PathBuf::from("/tmp"),
turn_id: "test".to_string(),
sub_id: None,
shell_environment_policy:
crate::tools::handlers::tool_handler::ShellEnvironmentPolicy::default(),
approval_policy: crate::tools::handlers::tool_handler::Constrained::allow_any(
crate::tools::handlers::tool_handler::ApprovalPolicy::default(),
),
codex_linux_sandbox_exe: None,
sandbox_policy: crate::tools::handlers::tool_handler::Constrained::allow_any(
Default::default(),
),
});
let invocation = ToolInvocation {
session,
turn,
tracker: None,
call_id: "test-call".to_string(),
tool_name: "exec".to_string(),
payload: ToolPayload::Function {
arguments: "{}".to_string(),
},
};
let result = facade.handle(invocation).await;
assert!(result.is_err());
}
#[tokio::test]
async fn same_context_both_facades() {
let tool = ToolFacade::new(TestAutoCtx);
let tool_result = tool
.execute(serde_json::json!({"via": "tool"}))
.await
.expect("tool facade should succeed");
assert!(tool_result.get("echoed").is_some());
let handler = HandlerFacade::new(TestAutoCtx);
let session: Arc<dyn crate::tools::handlers::tool_handler::ToolSession> = Arc::new(
crate::tools::handlers::adapter::DefaultToolSession::new(PathBuf::from("/tmp")),
);
let turn = Arc::new(crate::tools::handlers::tool_handler::TurnContext {
cwd: PathBuf::from("/tmp"),
turn_id: "test".to_string(),
sub_id: None,
shell_environment_policy:
crate::tools::handlers::tool_handler::ShellEnvironmentPolicy::default(),
approval_policy: crate::tools::handlers::tool_handler::Constrained::allow_any(
crate::tools::handlers::tool_handler::ApprovalPolicy::default(),
),
codex_linux_sandbox_exe: None,
sandbox_policy: crate::tools::handlers::tool_handler::Constrained::allow_any(
Default::default(),
),
});
let invocation = ToolInvocation {
session,
turn,
tracker: None,
call_id: "test-call".to_string(),
tool_name: "echo".to_string(),
payload: ToolPayload::Function {
arguments: r#"{"via":"handler"}"#.to_string(),
},
};
let handler_output = handler
.handle(invocation)
.await
.expect("handler facade should succeed");
assert!(handler_output.is_success());
let content = handler_output.content().expect("should have content");
assert!(content.contains("handler"));
}
#[tokio::test]
async fn tracing_logging_executes() {
let facade = ToolFacade::new(TestTracingCtx);
let result = facade
.execute(serde_json::json!({"test": true}))
.await
.expect("should succeed with tracing logging");
assert!(result.get("echoed").is_some());
}
#[tokio::test]
async fn cached_results_short_circuit_second_execute() {
let executions = Arc::new(AtomicUsize::new(0));
let facade = ToolFacade::new(TestCachingCtx::new(executions.clone()));
let first = facade
.execute(serde_json::json!({"query": "same"}))
.await
.expect("first execution should succeed");
let second = facade
.execute(serde_json::json!({"query": "same"}))
.await
.expect("second execution should succeed");
assert_eq!(executions.load(Ordering::SeqCst), 1);
assert_eq!(first, second);
}
#[tokio::test]
async fn cached_results_short_circuit_dual_output() {
let executions = Arc::new(AtomicUsize::new(0));
let facade = ToolFacade::new(TestCachingCtx::new(executions.clone()));
let first = facade
.execute_dual(serde_json::json!({"query": "same"}))
.await
.expect("first dual execution should succeed");
let second = facade
.execute_dual(serde_json::json!({"query": "same"}))
.await
.expect("second dual execution should succeed");
assert_eq!(executions.load(Ordering::SeqCst), 1);
assert_eq!(first.ui_content, second.ui_content);
assert_eq!(first.llm_content, second.llm_content);
}
#[tokio::test]
async fn retry_provider_retries_failed_execute() {
let executions = Arc::new(AtomicUsize::new(0));
let retry_policy = RetryPolicy {
max_attempts: 2,
initial_backoff: Duration::ZERO,
max_backoff: Duration::ZERO,
};
let facade = ToolFacade::new(TestRetryCtx::new(executions.clone(), retry_policy));
let result = facade
.execute(serde_json::json!({"retry": true}))
.await
.expect("retry should recover the transient failure");
assert_eq!(executions.load(Ordering::SeqCst), 2);
assert_eq!(
result.get("attempt").and_then(|value| value.as_u64()),
Some(2)
);
}
#[tokio::test]
async fn interactive_ctx_enables_sandbox() {
let ctx = InteractiveCtx::new(PathBuf::from("/workspace"));
let sandboxed = ComposableRuntime::run_with_sandbox(&ctx, "exec", "run cmd")
.await
.expect("should succeed");
assert!(sandboxed);
}
#[tokio::test]
async fn ci_ctx_auto_approves_with_sandbox() {
let ctx = CiCtx::new(PathBuf::from("/ci/workspace"));
let sandboxed = ComposableRuntime::run_with_sandbox(&ctx, "exec", "run cmd")
.await
.expect("should succeed");
assert!(sandboxed);
}
#[tokio::test]
async fn bench_ctx_no_sandbox() {
let sandboxed = ComposableRuntime::run_with_sandbox(&BenchCtx, "exec", "run cmd")
.await
.expect("should succeed");
assert!(!sandboxed);
}
struct SimpleTool;
#[async_trait]
impl Tool for SimpleTool {
async fn execute(&self, args: Value) -> Result<Value> {
Ok(serde_json::json!({
"tool_name": "simple",
"input": args,
"result": "ok"
}))
}
fn name(&self) -> &'static str {
"simple"
}
fn description(&self) -> &'static str {
"A simple test tool"
}
fn parameter_schema(&self) -> Option<Value> {
Some(serde_json::json!({
"type": "object",
"properties": {
"query": { "type": "string" }
}
}))
}
fn default_permission(&self) -> ToolPolicy {
ToolPolicy::Allow
}
fn is_mutating(&self) -> bool {
false
}
fn kind(&self) -> &'static str {
"test"
}
}
#[tokio::test]
async fn bridge_interactive_passthrough() {
let tool: Arc<dyn Tool> = Arc::new(SimpleTool);
let facade = wrap_tool_interactive(tool, PathBuf::from("/workspace"));
assert_eq!(facade.name(), "simple");
assert_eq!(facade.description(), "A simple test tool");
let result = facade
.execute(serde_json::json!({"query": "test"}))
.await
.expect("should succeed");
assert_eq!(result.get("result").and_then(|v| v.as_str()), Some("ok"));
assert_eq!(
result
.get("input")
.and_then(|v| v.get("query"))
.and_then(|v| v.as_str()),
Some("test")
);
}
#[tokio::test]
async fn bridge_ci_passthrough() {
let tool: Arc<dyn Tool> = Arc::new(SimpleTool);
let facade = wrap_tool_ci(tool, PathBuf::from("/ci"));
let result = facade
.execute(serde_json::json!({"key": "value"}))
.await
.expect("should succeed");
assert_eq!(result.get("result").and_then(|v| v.as_str()), Some("ok"));
}
#[tokio::test]
async fn bridge_passthrough_metadata_is_preserved() {
let tool: Arc<dyn Tool> = Arc::new(SimpleTool);
let facade = wrap_tool_interactive(tool, PathBuf::from("/workspace"));
assert!(facade.parameter_schema().is_some());
assert_eq!(facade.default_permission(), ToolPolicy::Allow);
assert!(!facade.is_mutating());
assert_eq!(facade.kind(), "test");
}
#[tokio::test]
async fn native_typed_tool_preserves_metadata_and_execution() {
let facade = wrap_native_tool_interactive(SimpleTool, PathBuf::from("/workspace"));
assert_eq!(facade.name(), "simple");
assert_eq!(facade.description(), "A simple test tool");
assert!(facade.parameter_schema().is_some());
assert_eq!(facade.default_permission(), ToolPolicy::Allow);
assert!(!facade.is_mutating());
assert_eq!(facade.kind(), "test");
let result = facade
.execute(serde_json::json!({"query": "native"}))
.await
.expect("should succeed");
assert_eq!(
result
.get("input")
.and_then(|v| v.get("query"))
.and_then(|v| v.as_str()),
Some("native")
);
}
#[tokio::test]
async fn bridge_dual_output() {
let tool: Arc<dyn Tool> = Arc::new(SimpleTool);
let facade = wrap_tool_interactive(tool, PathBuf::from("/workspace"));
let result = facade
.execute_dual(serde_json::json!({"x": 1}))
.await
.expect("should succeed");
assert!(result.success);
assert_eq!(result.tool_name, "simple");
}
#[tokio::test]
async fn bridge_handler_facade() {
let tool: Arc<dyn Tool> = Arc::new(SimpleTool);
let bridge_ctx = ToolBridgeCtx {
inner: tool,
runtime: InteractiveCtx::new(PathBuf::from("/workspace")),
};
let handler = HandlerFacade::new(bridge_ctx);
let session: Arc<dyn crate::tools::handlers::tool_handler::ToolSession> = Arc::new(
crate::tools::handlers::adapter::DefaultToolSession::new(PathBuf::from("/tmp")),
);
let turn = Arc::new(crate::tools::handlers::tool_handler::TurnContext {
cwd: PathBuf::from("/tmp"),
turn_id: "test".to_string(),
sub_id: None,
shell_environment_policy:
crate::tools::handlers::tool_handler::ShellEnvironmentPolicy::default(),
approval_policy: crate::tools::handlers::tool_handler::Constrained::allow_any(
crate::tools::handlers::tool_handler::ApprovalPolicy::default(),
),
codex_linux_sandbox_exe: None,
sandbox_policy: crate::tools::handlers::tool_handler::Constrained::allow_any(
Default::default(),
),
});
let invocation = ToolInvocation {
session,
turn,
tracker: None,
call_id: "bridge-test".to_string(),
tool_name: "simple".to_string(),
payload: ToolPayload::Function {
arguments: r#"{"via":"bridge"}"#.to_string(),
},
};
let output = handler.handle(invocation).await.expect("should succeed");
assert!(output.is_success());
let content = output.content().expect("should have content");
assert!(content.contains("bridge"));
}
#[tokio::test]
async fn bridge_ctx_delegates_components() {
let tool: Arc<dyn Tool> = Arc::new(SimpleTool);
let bridge = ToolBridgeCtx {
inner: tool,
runtime: InteractiveCtx::new(PathBuf::from("/workspace")),
};
let sandboxed = ComposableRuntime::run_with_sandbox(&bridge, "exec", "test")
.await
.expect("should succeed");
assert!(sandboxed);
}
}