use crate::reasoning::executor::{ActionExecutor, UnavailableToolExecutor};
use crate::toolclad::executor::ToolCladExecutor;
use crate::toolclad::manifest::{load_custom_types, load_manifests_from_dir_with_types};
use std::path::Path;
use std::sync::Arc;
pub fn build_tool_executor(tools_dir: &Path) -> Arc<dyn ActionExecutor> {
let project = project_for_tools(tools_dir);
match load_project_manifests(tools_dir, project) {
Ok((manifests, _)) if manifests.is_empty() => Arc::new(UnavailableToolExecutor),
Ok((manifests, custom_types)) => Arc::new(
ToolCladExecutor::with_custom_types(manifests, custom_types)
.with_project_scope(project)
.with_project_sandbox(project),
),
Err(error) => Arc::new(ToolCladExecutor::failed(error)),
}
}
#[allow(clippy::type_complexity)]
fn load_project_manifests(
tools_dir: &Path,
project: &Path,
) -> Result<
(
Vec<(String, crate::toolclad::Manifest)>,
std::collections::HashMap<String, crate::toolclad::manifest::ArgDef>,
),
String,
> {
let custom_types = load_custom_types(project)?;
let manifests = load_manifests_from_dir_with_types(tools_dir, &custom_types)?;
Ok((manifests, custom_types))
}
pub fn build_agent_tool_executor(
tools_dir: &Path,
settings: &dsl::AgentExecutionSettings,
) -> Result<Arc<dyn ActionExecutor>, String> {
let project = project_for_tools(tools_dir);
let boundary = crate::sandbox::command::CommandBoundary::load_for_agent(project, settings)?;
Ok(build_tool_executor_with_boundary(tools_dir, boundary))
}
pub(crate) fn build_tool_executor_with_boundary(
tools_dir: &Path,
boundary: crate::sandbox::command::CommandBoundary,
) -> Arc<dyn ActionExecutor> {
let project = project_for_tools(tools_dir);
match load_project_manifests(tools_dir, project) {
Ok((manifests, _)) if manifests.is_empty() => Arc::new(UnavailableToolExecutor),
Ok((manifests, custom_types)) => Arc::new(
ToolCladExecutor::with_custom_types(manifests, custom_types)
.with_project_scope(project)
.with_command_boundary(boundary),
),
Err(error) => Arc::new(ToolCladExecutor::failed(error)),
}
}
fn project_for_tools(tools_dir: &Path) -> &Path {
tools_dir
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or(Path::new("."))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_selection_is_required_even_without_tools() {
let project = tempfile::tempdir().unwrap();
let settings = dsl::resolve_execution_settings(
r#"agent a() { with sandbox = "firecracker" {} }"#,
"a",
)
.unwrap();
assert!(build_agent_tool_executor(&project.path().join("tools"), &settings).is_err());
}
#[test]
fn returns_unavailable_when_no_manifests() {
let dir = tempfile::tempdir().unwrap();
let exec = build_tool_executor(dir.path());
assert!(exec.tool_definitions().is_empty());
}
#[test]
fn returns_unavailable_when_dir_missing() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("does-not-exist");
let exec = build_tool_executor(&missing);
assert!(exec.tool_definitions().is_empty());
}
#[test]
fn returns_toolclad_when_manifests_present() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("greet.clad.toml"),
r#"
[tool]
name = "greet"
version = "1.0.0"
binary = "echo"
description = "g"
[args.message]
position = 1
required = true
type = "string"
description = "message to echo"
[command]
template = "echo {message}"
[output]
format = "text"
[output.schema]
type = "object"
"#,
)
.unwrap();
let exec = build_tool_executor(dir.path());
assert!(exec.tool_definitions().iter().any(|d| d.name == "greet"));
}
#[test]
fn a_manifest_load_failure_fails_configuration_loudly() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("typo.clad.toml"),
"[tool]\nname = \"t\"\nversion = \"1\"\nbinary = \"echo\"\ndescription = \"d\"\ntiemout_seconds = 5\n[command]\nexec = [\"echo\"]\n[output]\nformat = \"text\"\n",
)
.unwrap();
let exec = build_tool_executor(dir.path());
let err = exec.validate_configuration().unwrap_err();
assert!(err.contains("typo.clad.toml"), "{err}");
}
}