use std::sync::Arc;
use async_trait::async_trait;
use serde_json::Value;
use super::{
ParallelToolContext, RuntimeToolDescriptor, ToolAuthorizationPreview, ToolContext,
ToolDefinition, ToolExecutionCategory, ToolExecutionMode, ToolExecutor, ToolOutput, ToolResult,
};
impl<T: ToolDefinition + ?Sized> ToolDefinition for Box<T> {
fn descriptor(&self) -> RuntimeToolDescriptor {
(**self).descriptor()
}
}
impl<T: ToolDefinition + ?Sized> ToolDefinition for Arc<T> {
fn descriptor(&self) -> RuntimeToolDescriptor {
(**self).descriptor()
}
}
#[async_trait]
impl<T: ToolExecutor + ?Sized> ToolExecutor for Box<T> {
fn authorization_preview(
&self,
ctx: &ParallelToolContext,
input: &Value,
) -> Result<ToolAuthorizationPreview, String> {
(**self).authorization_preview(ctx, input)
}
fn execution_category(&self, input: &Value) -> ToolExecutionCategory {
(**self).execution_category(input)
}
fn execution_mode(&self, input: &Value) -> ToolExecutionMode {
(**self).execution_mode(input)
}
async fn execute(&self, ctx: ParallelToolContext, input: Value) -> ToolResult {
(**self).execute(ctx, input).await
}
async fn execute_mut(&self, ctx: ToolContext<'_>, input: Value) -> ToolResult {
(**self).execute_mut(ctx, input).await
}
async fn execute_output(
&self,
ctx: ParallelToolContext,
input: Value,
) -> Result<ToolOutput, String> {
(**self).execute_output(ctx, input).await
}
async fn execute_mut_output(
&self,
ctx: ToolContext<'_>,
input: Value,
) -> Result<ToolOutput, String> {
(**self).execute_mut_output(ctx, input).await
}
}
#[async_trait]
impl<T: ToolExecutor + ?Sized> ToolExecutor for Arc<T> {
fn authorization_preview(
&self,
ctx: &ParallelToolContext,
input: &Value,
) -> Result<ToolAuthorizationPreview, String> {
(**self).authorization_preview(ctx, input)
}
fn execution_category(&self, input: &Value) -> ToolExecutionCategory {
(**self).execution_category(input)
}
fn execution_mode(&self, input: &Value) -> ToolExecutionMode {
(**self).execution_mode(input)
}
async fn execute(&self, ctx: ParallelToolContext, input: Value) -> ToolResult {
(**self).execute(ctx, input).await
}
async fn execute_mut(&self, ctx: ToolContext<'_>, input: Value) -> ToolResult {
(**self).execute_mut(ctx, input).await
}
async fn execute_output(
&self,
ctx: ParallelToolContext,
input: Value,
) -> Result<ToolOutput, String> {
(**self).execute_output(ctx, input).await
}
async fn execute_mut_output(
&self,
ctx: ToolContext<'_>,
input: Value,
) -> Result<ToolOutput, String> {
(**self).execute_mut_output(ctx, input).await
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use async_trait::async_trait;
use serde_json::{Value, json};
use crate::{
agent::Agent,
test::MockRuntime,
tool::{
ExecutableTool, ParallelToolContext, RuntimeToolDescriptor, ToolApprovalCategory,
ToolAuthorizationPreview, ToolCapability, ToolContext, ToolDefinition, ToolDurability,
ToolExecutionCategory, ToolExecutionMode, ToolExecutor, ToolOutput, ToolResult,
ToolResultContent, ToolSideEffectLevel,
},
};
const PROBE_TOOL_NAME: &str = "forwarding_probe";
struct ProbeTool;
impl ToolDefinition for ProbeTool {
fn descriptor(&self) -> RuntimeToolDescriptor {
RuntimeToolDescriptor::builder(PROBE_TOOL_NAME)
.description("Probe every forwarded method")
.input_schema(json!({ "type": "object", "properties": {} }))
.capabilities([ToolCapability::ReadOnly])
.side_effect_level(ToolSideEffectLevel::None)
.durability(ToolDurability::Ephemeral)
.execution_category(ToolExecutionCategory::ReadOnlyParallel)
.approval_category(ToolApprovalCategory::Default)
.build()
}
}
#[async_trait]
impl ToolExecutor for ProbeTool {
fn authorization_preview(
&self,
ctx: &ParallelToolContext,
input: &Value,
) -> Result<ToolAuthorizationPreview, String> {
Ok(ToolAuthorizationPreview {
working_directory: ctx.working_directory().to_path_buf(),
capabilities: vec![ToolCapability::ProcessExec],
side_effect_level: ToolSideEffectLevel::External,
durability: ToolDurability::Persistent,
execution_category: ToolExecutionCategory::Delegation,
approval_category: ToolApprovalCategory::Process,
raw_input: input.clone(),
structured_input: json!({ "normalized": input }),
})
}
fn execution_category(&self, _input: &Value) -> ToolExecutionCategory {
ToolExecutionCategory::BackgroundJob
}
fn execution_mode(&self, _input: &Value) -> ToolExecutionMode {
ToolExecutionMode::Parallel
}
async fn execute(&self, _ctx: ParallelToolContext, _input: Value) -> ToolResult {
Ok("probe::execute".to_string())
}
async fn execute_mut(&self, _ctx: ToolContext<'_>, _input: Value) -> ToolResult {
Ok("probe::execute_mut".to_string())
}
async fn execute_output(
&self,
_ctx: ParallelToolContext,
_input: Value,
) -> Result<ToolOutput, String> {
Ok(
ToolOutput::structured(json!({ "from": "probe::execute_output" }))
.with_details(json!({ "lane": "parallel" })),
)
}
async fn execute_mut_output(
&self,
_ctx: ToolContext<'_>,
_input: Value,
) -> Result<ToolOutput, String> {
Ok(
ToolOutput::structured(json!({ "from": "probe::execute_mut_output" }))
.with_details(json!({ "lane": "exclusive" }))
.terminating(),
)
}
}
fn parallel_context(agent: &Agent) -> ParallelToolContext {
ParallelToolContext {
agent_id: agent.id().to_string(),
tool_call_id: "probe-call".to_string(),
tool_name: PROBE_TOOL_NAME.to_string(),
working_directory: std::env::temp_dir(),
runtime: agent.runtime_handle(),
subagent_template: agent.disposable_subagent_template(),
agent_name: agent.name().to_string(),
model: agent.model().to_string(),
history_len: agent.history().len(),
tasks: agent.tasks().to_vec(),
event_tx: agent.event_sender(),
run_options: crate::runtime::RunOptions::default(),
}
}
fn exclusive_context(agent: &mut Agent) -> ToolContext<'_> {
let agent_id = agent.id().to_string();
let runtime = agent.runtime_handle();
let event_tx = agent.event_sender();
ToolContext {
agent_id,
tool_call_id: "probe-call".to_string(),
tool_name: PROBE_TOOL_NAME.to_string(),
working_directory: std::env::temp_dir(),
runtime,
agent,
event_tx,
run_options: crate::runtime::RunOptions::default(),
}
}
async fn assert_every_method_forwards<T>(tool: &T, agent: &mut Agent)
where
T: ExecutableTool + ?Sized,
{
let input = json!({ "value": "hi" });
let descriptor = tool.descriptor();
assert_eq!(descriptor.provider.name, PROBE_TOOL_NAME);
assert_eq!(descriptor.capabilities, vec![ToolCapability::ReadOnly]);
let preview = tool
.authorization_preview(¶llel_context(agent), &input)
.expect("forwarded authorization preview");
assert_eq!(preview.capabilities, vec![ToolCapability::ProcessExec]);
assert_eq!(preview.side_effect_level, ToolSideEffectLevel::External);
assert_eq!(preview.durability, ToolDurability::Persistent);
assert_eq!(
preview.execution_category,
ToolExecutionCategory::Delegation
);
assert_eq!(preview.approval_category, ToolApprovalCategory::Process);
assert_eq!(preview.raw_input, input);
assert_eq!(preview.structured_input, json!({ "normalized": input }));
assert_eq!(
tool.execution_category(&input),
ToolExecutionCategory::BackgroundJob
);
assert_eq!(tool.execution_mode(&input), ToolExecutionMode::Parallel);
assert_eq!(
tool.execute(parallel_context(agent), input.clone()).await,
Ok("probe::execute".to_string())
);
let output = tool
.execute_output(parallel_context(agent), input.clone())
.await
.expect("forwarded parallel structured output");
assert_eq!(
output.content,
ToolResultContent::Structured(json!({ "from": "probe::execute_output" }))
);
assert_eq!(output.details, Some(json!({ "lane": "parallel" })));
assert!(!output.terminate);
assert_eq!(
tool.execute_mut(exclusive_context(agent), input.clone())
.await,
Ok("probe::execute_mut".to_string())
);
let output = tool
.execute_mut_output(exclusive_context(agent), input.clone())
.await
.expect("forwarded exclusive structured output");
assert_eq!(
output.content,
ToolResultContent::Structured(json!({ "from": "probe::execute_mut_output" }))
);
assert_eq!(output.details, Some(json!({ "lane": "exclusive" })));
assert!(output.terminate);
}
async fn probe_agent() -> (MockRuntime, Agent) {
let mock = MockRuntime::builder().build().expect("build mock runtime");
let agent = mock
.runtime()
.spawn("forwarding-probe", mock.model())
.expect("spawn probe agent");
(mock, agent)
}
#[tokio::test]
async fn a_boxed_tool_forwards_every_method() {
let (_mock, mut agent) = probe_agent().await;
let tool: Box<dyn ExecutableTool> = Box::new(ProbeTool);
assert_every_method_forwards(&tool, &mut agent).await;
}
#[tokio::test]
async fn a_boxed_sized_tool_forwards_every_method() {
let (_mock, mut agent) = probe_agent().await;
let tool = Box::new(ProbeTool);
assert_every_method_forwards(&tool, &mut agent).await;
}
#[tokio::test]
async fn a_shared_tool_forwards_every_method() {
let (_mock, mut agent) = probe_agent().await;
let tool = Arc::new(ProbeTool);
assert_every_method_forwards(&tool, &mut agent).await;
}
#[tokio::test]
async fn a_shared_unsized_tool_forwards_every_method() {
let (_mock, mut agent) = probe_agent().await;
let tool: Arc<dyn ExecutableTool> = Arc::new(ProbeTool);
assert_every_method_forwards(&tool, &mut agent).await;
}
#[tokio::test]
async fn a_runtime_registers_boxed_and_shared_tools() {
let (mock, agent) = probe_agent().await;
mock.runtime()
.register_tool(Box::new(ProbeTool) as Box<dyn ExecutableTool>);
let registered = agent
.runtime_handle()
.get_tool(PROBE_TOOL_NAME)
.expect("boxed tool registered");
assert_eq!(registered.descriptor().provider.name, PROBE_TOOL_NAME);
assert_eq!(
registered
.authorization_preview(¶llel_context(&agent), &json!({}))
.expect("preview through the registered boxed tool")
.side_effect_level,
ToolSideEffectLevel::External,
);
mock.runtime().unregister_tool(PROBE_TOOL_NAME);
let shared = Arc::new(ProbeTool);
mock.runtime().register_tool(Arc::clone(&shared));
let registered = agent
.runtime_handle()
.get_tool(PROBE_TOOL_NAME)
.expect("shared tool registered");
assert_eq!(
registered
.authorization_preview(¶llel_context(&agent), &json!({}))
.expect("preview through the registered shared tool")
.side_effect_level,
ToolSideEffectLevel::External,
);
}
}