use std::sync::{Arc, Mutex};
use crate::{ToolError, ToolExecutor};
use serde_json::{json, Value};
use crate::tool_groups::{ToolGroup, ToolRegistry};
use crate::tools::dispatch_tool;
pub struct SecretaryToolExecutor {
registry: Option<Arc<Mutex<ToolRegistry>>>,
}
impl SecretaryToolExecutor {
#[must_use]
pub fn with_registry(registry: Arc<Mutex<ToolRegistry>>) -> Self {
Self {
registry: Some(registry),
}
}
#[must_use]
pub fn stateless() -> Self {
Self { registry: None }
}
#[must_use]
pub fn new() -> Self {
Self::stateless()
}
}
impl Default for SecretaryToolExecutor {
fn default() -> Self {
Self::stateless()
}
}
impl ToolExecutor for SecretaryToolExecutor {
fn execute(&mut self, tool_name: &str, input: &str) -> Result<String, ToolError> {
if tool_name == "enable_tools" {
return run_enable_tools(self.registry.as_ref(), input).map_err(ToolError::new);
}
dispatch_tool(tool_name, input)
.map(|result| format!("[tool:{tool_name}] {result}"))
.map_err(ToolError::new)
}
}
fn run_enable_tools(
registry: Option<&Arc<Mutex<ToolRegistry>>>,
input: &str,
) -> Result<String, String> {
let Some(registry) = registry else {
return Err(
"enable_tools is not available in this runtime (stateless executor)".to_string(),
);
};
let v: Value = serde_json::from_str(input)
.map_err(|e| format!("enable_tools: invalid JSON input ({e}): {input}"))?;
let group_name = v
.get("group")
.and_then(Value::as_str)
.map(str::trim)
.unwrap_or_default();
if group_name.is_empty() {
let mut reg = lock_registry(registry);
let newly = reg.enable_coding_core();
let groups: Vec<&str> = ToolGroup::coding_core().iter().map(|g| g.name()).collect();
let tools: Vec<String> = ToolGroup::coding_core()
.into_iter()
.flat_map(|g| reg.group_tool_names(g))
.collect();
let current_count = reg.current_len();
return Ok(json!({
"ok": true,
"note": "No 'group' was provided, so I enabled the coding core (files, search, advanced, quality). Call these tools directly on your next turn. For a different group (e.g. git, github) call enable_tools with {\"group\":\"git\"}.",
"groups_enabled": groups,
"newly_enabled_groups": newly,
"tools_now_available": tools,
"total_advertised_tools": current_count,
})
.to_string());
}
let group = ToolGroup::parse(group_name).ok_or_else(|| {
let available: Vec<&str> = ToolGroup::all().iter().map(|g| g.name()).collect();
format!(
"enable_tools: unknown group '{group_name}' — available: {}",
available.join(", ")
)
})?;
let mut reg = lock_registry(registry);
let newly_enabled = reg.enable(group);
let tool_names = reg.group_tool_names(group);
let current_count = reg.current_len();
Ok(json!({
"ok": true,
"group": group.name(),
"already_enabled": !newly_enabled,
"tools_now_available": tool_names,
"total_advertised_tools": current_count,
"note": "The new tools take effect on the next model call — call them directly on your next turn.",
})
.to_string())
}
fn lock_registry(registry: &Arc<Mutex<ToolRegistry>>) -> std::sync::MutexGuard<'_, ToolRegistry> {
match registry.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ToolExecutor;
#[test]
fn stateless_executor_rejects_enable_tools() {
let mut exec = SecretaryToolExecutor::stateless();
let result = exec.execute("enable_tools", r#"{"group":"git"}"#);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("not available"),
"expected 'not available', got: {err}"
);
}
#[test]
fn stateless_executor_dispatches_core_tool() {
let mut exec = SecretaryToolExecutor::stateless();
let result = exec.execute("get_current_time", "{}");
assert!(result.is_ok(), "core tools should still work: {result:?}");
assert!(result.unwrap().contains("iso8601"));
}
#[test]
fn wired_executor_enables_git_group() {
let registry = Arc::new(Mutex::new(ToolRegistry::new()));
let mut exec = SecretaryToolExecutor::with_registry(registry.clone());
assert!(!registry.lock().unwrap().is_enabled(ToolGroup::Git));
let result = exec.execute("enable_tools", r#"{"group":"git"}"#).unwrap();
assert!(result.contains("\"ok\":true"));
assert!(result.contains("git_status"));
assert!(registry.lock().unwrap().is_enabled(ToolGroup::Git));
}
#[test]
fn wired_executor_reports_already_enabled_on_second_call() {
let registry = Arc::new(Mutex::new(ToolRegistry::new()));
let mut exec = SecretaryToolExecutor::with_registry(registry);
let first = exec.execute("enable_tools", r#"{"group":"ide"}"#).unwrap();
assert!(first.contains("\"already_enabled\":false"));
let second = exec.execute("enable_tools", r#"{"group":"ide"}"#).unwrap();
assert!(second.contains("\"already_enabled\":true"));
}
#[test]
fn wired_executor_unknown_group_errors_clearly() {
let registry = Arc::new(Mutex::new(ToolRegistry::new()));
let mut exec = SecretaryToolExecutor::with_registry(registry);
let err = exec
.execute("enable_tools", r#"{"group":"does-not-exist-xyz"}"#)
.unwrap_err()
.to_string();
assert!(err.contains("unknown group"), "got: {err}");
for group in ToolGroup::all() {
assert!(
err.contains(group.name()),
"error should list group '{}': {err}",
group.name()
);
}
}
#[test]
fn wired_executor_missing_group_enables_coding_core() {
for input in ["{}", r#"{"group":""}"#, r#"{"group":" "}"#] {
let registry = Arc::new(Mutex::new(ToolRegistry::new()));
let mut exec = SecretaryToolExecutor::with_registry(Arc::clone(®istry));
let out = exec
.execute("enable_tools", input)
.unwrap_or_else(|e| panic!("input {input:?} should not error: {e}"));
assert!(out.contains("\"ok\":true"), "input {input:?}: {out}");
for tool in [
"read_file",
"write_file",
"edit_file",
"grep_search",
"run_tests",
] {
assert!(out.contains(tool), "input {input:?} missing {tool}: {out}");
}
let reg = registry.lock().unwrap();
for g in ToolGroup::coding_core() {
assert!(
reg.is_enabled(g),
"group {g:?} not enabled for input {input:?}"
);
}
}
}
#[test]
fn wired_executor_bad_json_errors() {
let registry = Arc::new(Mutex::new(ToolRegistry::new()));
let mut exec = SecretaryToolExecutor::with_registry(registry);
let err = exec
.execute("enable_tools", "not json at all")
.unwrap_err()
.to_string();
assert!(err.contains("invalid JSON"), "got: {err}");
}
#[test]
fn wired_executor_still_dispatches_non_meta_tools() {
let registry = Arc::new(Mutex::new(ToolRegistry::new()));
let mut exec = SecretaryToolExecutor::with_registry(registry);
let result = exec.execute("get_current_time", "{}").unwrap();
assert!(result.contains("iso8601"));
}
}