Skip to main content

navi_core/harness_pack/
capability.rs

1//! Capability inventory and developer-facing capability card.
2
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeSet;
5
6/// Snapshot of what NAVI can expose this session/build.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
8pub struct CapabilityInventory {
9    /// Direct (always-schema) tool names.
10    pub direct_tools: Vec<String>,
11    /// Deferred / discoverable tool names (via tool_search).
12    pub deferred_tools: Vec<String>,
13    /// All registered tool names (union).
14    pub all_tools: Vec<String>,
15    pub goals_enabled: bool,
16    pub browser_available: bool,
17    pub max_auto_continue_turns: u32,
18    pub plugins: Vec<String>,
19    pub mcp_servers: Vec<String>,
20    pub harnesses_ready: Vec<String>,
21}
22
23/// Build inventory from known tool name lists and feature flags.
24///
25/// Pure function so tests can fix inputs without a live ToolExecutor.
26pub fn inventory_from_tool_names(
27    direct: impl IntoIterator<Item = impl Into<String>>,
28    deferred: impl IntoIterator<Item = impl Into<String>>,
29    goals_enabled: bool,
30    browser_available: bool,
31    max_auto_continue_turns: u32,
32    plugins: impl IntoIterator<Item = impl Into<String>>,
33    mcp_servers: impl IntoIterator<Item = impl Into<String>>,
34    harnesses_ready: impl IntoIterator<Item = impl Into<String>>,
35) -> CapabilityInventory {
36    let mut direct_tools: Vec<String> = direct.into_iter().map(Into::into).collect();
37    let mut deferred_tools: Vec<String> = deferred.into_iter().map(Into::into).collect();
38    direct_tools.sort();
39    direct_tools.dedup();
40    deferred_tools.sort();
41    deferred_tools.dedup();
42    let mut all: BTreeSet<String> = direct_tools.iter().cloned().collect();
43    for t in &deferred_tools {
44        all.insert(t.clone());
45    }
46    let mut plugins: Vec<String> = plugins.into_iter().map(Into::into).collect();
47    plugins.sort();
48    plugins.dedup();
49    let mut mcp_servers: Vec<String> = mcp_servers.into_iter().map(Into::into).collect();
50    mcp_servers.sort();
51    mcp_servers.dedup();
52    let mut harnesses_ready: Vec<String> = harnesses_ready.into_iter().map(Into::into).collect();
53    harnesses_ready.sort();
54    harnesses_ready.dedup();
55    CapabilityInventory {
56        direct_tools,
57        deferred_tools,
58        all_tools: all.into_iter().collect(),
59        goals_enabled,
60        browser_available,
61        max_auto_continue_turns,
62        plugins,
63        mcp_servers,
64        harnesses_ready,
65    }
66}
67
68/// Filter tool names to those present in the inventory (case-sensitive match on name).
69pub fn filter_tools_to_inventory(
70    requested: &[String],
71    inventory: &CapabilityInventory,
72) -> Vec<String> {
73    let set: BTreeSet<&str> = inventory.all_tools.iter().map(|s| s.as_str()).collect();
74    let mut out: Vec<String> = requested
75        .iter()
76        .filter(|t| set.contains(t.as_str()))
77        .cloned()
78        .collect();
79    out.sort();
80    out.dedup();
81    out
82}
83
84/// Render a short developer-facing capability card (stable for fixed inputs).
85pub fn capability_card(inv: &CapabilityInventory) -> String {
86    let mut lines = Vec::new();
87    lines.push("## NAVI capabilities (this session)".to_string());
88    lines.push(format!(
89        "- goals: {} | max_auto_continue: {}",
90        if inv.goals_enabled {
91            "enabled"
92        } else {
93            "disabled"
94        },
95        inv.max_auto_continue_turns
96    ));
97    lines.push(format!(
98        "- browser: {}",
99        if inv.browser_available {
100            "available"
101        } else {
102            "unavailable"
103        }
104    ));
105    lines.push(format!("- tools.direct: [{}]", inv.direct_tools.join(", ")));
106    if !inv.deferred_tools.is_empty() {
107        lines.push(format!(
108            "- tools.deferred: discover via tool_search ([{}])",
109            inv.deferred_tools.join(", ")
110        ));
111    }
112    if !inv.plugins.is_empty() {
113        lines.push(format!("- plugins.installed: [{}]", inv.plugins.join(", ")));
114    }
115    if !inv.mcp_servers.is_empty() {
116        lines.push(format!("- mcp.connected: [{}]", inv.mcp_servers.join(", ")));
117    }
118    if !inv.harnesses_ready.is_empty() {
119        lines.push(format!(
120            "- harnesses.ready: [{}]",
121            inv.harnesses_ready.join(", ")
122        ));
123    }
124    lines.join("\n")
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    fn sample() -> CapabilityInventory {
132        inventory_from_tool_names(
133            ["search", "read_file", "edit", "bash"],
134            ["browser", "subagent", "code"],
135            true,
136            true,
137            50,
138            ["hello-echo"],
139            ["memory"],
140            ["design-loop"],
141        )
142    }
143
144    #[test]
145    fn filter_drops_unknown_tools() {
146        let inv = sample();
147        let filtered = filter_tools_to_inventory(
148            &["search".into(), "a11y_audit".into(), "browser".into()],
149            &inv,
150        );
151        assert_eq!(filtered, vec!["browser".to_string(), "search".to_string()]);
152    }
153
154    #[test]
155    fn card_stable_for_fixed_inputs() {
156        let a = capability_card(&sample());
157        let b = capability_card(&sample());
158        assert_eq!(a, b);
159        assert!(a.contains("goals: enabled"));
160        assert!(a.contains("browser: available"));
161        assert!(a.contains("search"));
162        assert!(a.contains("tool_search"));
163        assert!(a.contains("design-loop"));
164    }
165}