use std::sync::Arc;
use anyhow::{Context, Result};
use clap::Subcommand;
use merlion_core::ToolRegistry;
use merlion_memory::MemoryStore;
use merlion_tools::skill_tools::SkillToolsConfig;
#[derive(Debug, Subcommand)]
pub enum ToolsAction {
List,
}
pub async fn run(action: ToolsAction) -> Result<()> {
match action {
ToolsAction::List => list().await,
}
}
async fn list() -> Result<()> {
let sections = build_sections().context("constructing tool registries")?;
for (heading, names) in sections {
println!("# {heading}");
if names.is_empty() {
println!(" (none)");
} else {
let width = names.iter().map(|(n, _)| n.len()).max().unwrap_or(0);
for (name, desc) in names {
let first_line = desc.lines().next().unwrap_or("").trim();
println!(" {name:<width$} {first_line}", name = name, width = width);
}
}
println!();
}
Ok(())
}
type ToolEntry = (String, String);
type Section = (&'static str, Vec<ToolEntry>);
fn build_sections() -> Result<Vec<Section>> {
let scratch = tempfile::tempdir().context("create scratch tempdir for tool inspection")?;
let core = {
let mut reg = ToolRegistry::new();
merlion_tools::register_defaults(&mut reg);
schemas(®)
};
let memory = {
let mut reg = ToolRegistry::new();
let store = Arc::new(
MemoryStore::open(scratch.path().join("memory"))
.context("open scratch memory store")?,
);
merlion_tools::register_memory(&mut reg, store);
schemas(®)
};
let skills = {
let mut reg = ToolRegistry::new();
let cfg = SkillToolsConfig::new(scratch.path().join("skills"));
merlion_tools::register_skill_tools(&mut reg, cfg);
schemas(®)
};
let sandbox = {
let mut reg = ToolRegistry::new();
merlion_tools::register_sandbox_bash(&mut reg);
schemas(®)
};
let subagent = {
let mut reg = ToolRegistry::new();
let _handle = merlion_tools::register_task_tool(&mut reg);
schemas(®)
};
Ok(vec![
("Core", core),
("Memory", memory),
("Skills", skills),
("Sandbox", sandbox),
("Subagent", subagent),
])
}
fn schemas(reg: &ToolRegistry) -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = reg
.schemas()
.into_iter()
.map(|s| (s.name, s.description))
.collect();
out.sort_by(|a, b| a.0.cmp(&b.0));
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_sections_covers_every_helper_and_returns_nonempty_for_each() {
let sections = build_sections().expect("build_sections");
let names: Vec<&'static str> = sections.iter().map(|(h, _)| *h).collect();
assert_eq!(
names,
vec!["Core", "Memory", "Skills", "Sandbox", "Subagent"]
);
for (heading, tools) in §ions {
assert!(
!tools.is_empty(),
"section `{heading}` should have at least one tool",
);
for (name, desc) in tools {
assert!(!name.is_empty(), "tool in `{heading}` has empty name");
assert!(
!desc.is_empty(),
"tool `{name}` in `{heading}` has empty description",
);
}
}
}
#[test]
fn core_section_contains_canonical_tools() {
let sections = build_sections().expect("build_sections");
let core: &Vec<(String, String)> = §ions
.iter()
.find(|(h, _)| *h == "Core")
.expect("Core section")
.1;
let names: Vec<&str> = core.iter().map(|(n, _)| n.as_str()).collect();
for required in ["bash", "read", "write", "edit", "ls", "grep", "glob"] {
assert!(
names.contains(&required),
"Core section missing `{required}`; got {names:?}",
);
}
}
}