navi_core/harness_pack/
capability.rs1use serde::{Deserialize, Serialize};
4use std::collections::BTreeSet;
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
8pub struct CapabilityInventory {
9 pub direct_tools: Vec<String>,
11 pub deferred_tools: Vec<String>,
13 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
23pub 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
68pub 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
84pub 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}