use std::fmt;
pub const TOOL_GROUP_PREFIX: char = '@';
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ToolGroup {
All,
Builtin,
Subagent,
Scripts,
Mcp,
}
impl ToolGroup {
pub const ALL: &'static [ToolGroup] = &[
ToolGroup::All,
ToolGroup::Builtin,
ToolGroup::Subagent,
ToolGroup::Scripts,
ToolGroup::Mcp,
];
pub fn token(self) -> &'static str {
match self {
ToolGroup::All => "@all",
ToolGroup::Builtin => "@builtin",
ToolGroup::Subagent => "@subagent",
ToolGroup::Scripts => "@scripts",
ToolGroup::Mcp => "@mcp",
}
}
pub fn describe(self) -> &'static str {
match self {
ToolGroup::All => "every tool this install has: built in, sub-agent, scripts, and MCP",
ToolGroup::Builtin => "every tool compiled into Leviath",
ToolGroup::Subagent => "the sub-agent tools: spawn, check, wait, send, kill",
ToolGroup::Scripts => "every Rhai script tool, the agent's own and the global ones",
ToolGroup::Mcp => "every tool every connected MCP server advertises",
}
}
pub fn parse(entry: &str) -> Option<ToolGroup> {
ToolGroup::ALL.iter().copied().find(|g| g.token() == entry)
}
pub fn covers(self, source: ToolGroup) -> bool {
self == ToolGroup::All || self == source
}
}
impl fmt::Display for ToolGroup {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.token())
}
}
pub fn is_tool_group_token(entry: &str) -> bool {
entry.starts_with(TOOL_GROUP_PREFIX)
}
pub fn unknown_group(entry: &str) -> bool {
is_tool_group_token(entry) && ToolGroup::parse(entry).is_none()
}
pub fn groups_in(entries: &[String]) -> Vec<ToolGroup> {
let mut groups = Vec::new();
for entry in entries {
if let Some(group) = ToolGroup::parse(entry)
&& !groups.contains(&group)
{
groups.push(group);
}
}
groups
}
pub fn group_tokens_list() -> String {
ToolGroup::ALL
.iter()
.map(|g| g.token())
.collect::<Vec<_>>()
.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_token_round_trips_through_parse() {
for group in ToolGroup::ALL {
assert_eq!(ToolGroup::parse(group.token()), Some(*group));
assert!(group.token().starts_with(TOOL_GROUP_PREFIX));
assert!(!group.describe().is_empty());
assert_eq!(group.to_string(), group.token());
}
}
#[test]
fn a_tool_name_is_not_a_group() {
assert_eq!(ToolGroup::parse("read_file"), None);
assert_eq!(ToolGroup::parse("github__create_issue"), None);
assert!(!is_tool_group_token("read_file"));
assert!(!unknown_group("read_file"));
}
#[test]
fn a_misspelled_group_is_unknown_not_a_tool() {
for entry in ["@builtins", "@ALL", "@", "@mcp "] {
assert_eq!(ToolGroup::parse(entry), None, "{entry}");
assert!(is_tool_group_token(entry), "{entry}");
assert!(unknown_group(entry), "{entry}");
}
assert!(!unknown_group("@all"));
}
#[test]
fn all_covers_every_source_and_a_source_covers_itself() {
for source in ToolGroup::ALL {
assert!(ToolGroup::All.covers(*source));
assert!(source.covers(*source));
}
assert!(!ToolGroup::Builtin.covers(ToolGroup::Scripts));
assert!(!ToolGroup::Mcp.covers(ToolGroup::Builtin));
}
#[test]
fn groups_in_keeps_list_order_and_drops_repeats() {
let entries: Vec<String> = ["@scripts", "read_file", "@builtin", "@scripts", "@nope"]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(
groups_in(&entries),
vec![ToolGroup::Scripts, ToolGroup::Builtin]
);
assert!(groups_in(&[]).is_empty());
}
#[test]
fn the_token_list_names_every_group() {
let list = group_tokens_list();
for group in ToolGroup::ALL {
assert!(list.contains(group.token()), "{list}");
}
}
}