use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct CapabilityInventory {
pub direct_tools: Vec<String>,
pub deferred_tools: Vec<String>,
pub all_tools: Vec<String>,
pub goals_enabled: bool,
pub browser_available: bool,
pub max_auto_continue_turns: u32,
pub plugins: Vec<String>,
pub mcp_servers: Vec<String>,
pub harnesses_ready: Vec<String>,
}
pub fn inventory_from_tool_names(
direct: impl IntoIterator<Item = impl Into<String>>,
deferred: impl IntoIterator<Item = impl Into<String>>,
goals_enabled: bool,
browser_available: bool,
max_auto_continue_turns: u32,
plugins: impl IntoIterator<Item = impl Into<String>>,
mcp_servers: impl IntoIterator<Item = impl Into<String>>,
harnesses_ready: impl IntoIterator<Item = impl Into<String>>,
) -> CapabilityInventory {
let mut direct_tools: Vec<String> = direct.into_iter().map(Into::into).collect();
let mut deferred_tools: Vec<String> = deferred.into_iter().map(Into::into).collect();
direct_tools.sort();
direct_tools.dedup();
deferred_tools.sort();
deferred_tools.dedup();
let mut all: BTreeSet<String> = direct_tools.iter().cloned().collect();
for t in &deferred_tools {
all.insert(t.clone());
}
let mut plugins: Vec<String> = plugins.into_iter().map(Into::into).collect();
plugins.sort();
plugins.dedup();
let mut mcp_servers: Vec<String> = mcp_servers.into_iter().map(Into::into).collect();
mcp_servers.sort();
mcp_servers.dedup();
let mut harnesses_ready: Vec<String> = harnesses_ready.into_iter().map(Into::into).collect();
harnesses_ready.sort();
harnesses_ready.dedup();
CapabilityInventory {
direct_tools,
deferred_tools,
all_tools: all.into_iter().collect(),
goals_enabled,
browser_available,
max_auto_continue_turns,
plugins,
mcp_servers,
harnesses_ready,
}
}
pub fn filter_tools_to_inventory(
requested: &[String],
inventory: &CapabilityInventory,
) -> Vec<String> {
let set: BTreeSet<&str> = inventory.all_tools.iter().map(|s| s.as_str()).collect();
let mut out: Vec<String> = requested
.iter()
.filter(|t| set.contains(t.as_str()))
.cloned()
.collect();
out.sort();
out.dedup();
out
}
pub fn capability_card(inv: &CapabilityInventory) -> String {
let mut lines = Vec::new();
lines.push("## NAVI capabilities (this session)".to_string());
lines.push(format!(
"- goals: {} | max_auto_continue: {}",
if inv.goals_enabled {
"enabled"
} else {
"disabled"
},
inv.max_auto_continue_turns
));
lines.push(format!(
"- browser: {}",
if inv.browser_available {
"available"
} else {
"unavailable"
}
));
lines.push(format!("- tools.direct: [{}]", inv.direct_tools.join(", ")));
if !inv.deferred_tools.is_empty() {
lines.push(format!(
"- tools.deferred: discover via tool_search ([{}])",
inv.deferred_tools.join(", ")
));
}
if !inv.plugins.is_empty() {
lines.push(format!("- plugins.installed: [{}]", inv.plugins.join(", ")));
}
if !inv.mcp_servers.is_empty() {
lines.push(format!("- mcp.connected: [{}]", inv.mcp_servers.join(", ")));
}
if !inv.harnesses_ready.is_empty() {
lines.push(format!(
"- harnesses.ready: [{}]",
inv.harnesses_ready.join(", ")
));
}
lines.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> CapabilityInventory {
inventory_from_tool_names(
["search", "read_file", "edit", "bash"],
["browser", "subagent", "code"],
true,
true,
50,
["hello-echo"],
["memory"],
["design-loop"],
)
}
#[test]
fn filter_drops_unknown_tools() {
let inv = sample();
let filtered = filter_tools_to_inventory(
&["search".into(), "a11y_audit".into(), "browser".into()],
&inv,
);
assert_eq!(filtered, vec!["browser".to_string(), "search".to_string()]);
}
#[test]
fn card_stable_for_fixed_inputs() {
let a = capability_card(&sample());
let b = capability_card(&sample());
assert_eq!(a, b);
assert!(a.contains("goals: enabled"));
assert!(a.contains("browser: available"));
assert!(a.contains("search"));
assert!(a.contains("tool_search"));
assert!(a.contains("design-loop"));
}
}