use std::collections::HashSet;
use bob_core::types::ToolDescriptor;
#[derive(Debug)]
pub struct ProgressiveToolView {
all_tools: Vec<ToolDescriptor>,
activated: HashSet<String>,
}
impl ProgressiveToolView {
#[must_use]
pub fn new(tools: Vec<ToolDescriptor>) -> Self {
Self { all_tools: tools, activated: HashSet::new() }
}
pub fn activate(&mut self, tool_id: &str) {
self.activated.insert(tool_id.to_string());
}
pub fn activate_hints(&mut self, text: &str) {
for tool in &self.all_tools {
let hint = format!("${}", tool.id);
if text.contains(&hint) {
self.activated.insert(tool.id.clone());
}
}
}
#[must_use]
pub fn is_activated(&self, tool_id: &str) -> bool {
self.activated.contains(tool_id)
}
#[must_use]
pub fn activated_count(&self) -> usize {
self.activated.len()
}
#[must_use]
pub fn total_count(&self) -> usize {
self.all_tools.len()
}
#[must_use]
pub fn summary_prompt(&self) -> String {
if self.all_tools.is_empty() {
return String::new();
}
let mut buf =
String::from("<tool_view>\nAvailable tools (use $name to request full schema):\n");
for tool in &self.all_tools {
let marker = if self.activated.contains(&tool.id) { " [active]" } else { "" };
buf.push_str(&format!(" - {}: {}{}\n", tool.id, tool.description, marker));
}
buf.push_str("</tool_view>");
buf
}
#[must_use]
pub fn activated_tools(&self) -> Vec<ToolDescriptor> {
self.all_tools.iter().filter(|t| self.activated.contains(&t.id)).cloned().collect()
}
#[must_use]
pub fn all_tools(&self) -> &[ToolDescriptor] {
&self.all_tools
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
fn make_tool(id: &str, desc: &str) -> ToolDescriptor {
ToolDescriptor::new(id, desc).with_input_schema(
json!({"type": "object", "properties": {"path": {"type": "string"}}}),
)
}
#[test]
fn new_view_has_no_activated_tools() {
let view = ProgressiveToolView::new(vec![
make_tool("file.read", "Read a file"),
make_tool("shell.exec", "Run a command"),
]);
assert_eq!(view.activated_count(), 0);
assert_eq!(view.total_count(), 2);
assert!(view.activated_tools().is_empty());
}
#[test]
fn activate_adds_tool_to_active_set() {
let mut view = ProgressiveToolView::new(vec![
make_tool("file.read", "Read a file"),
make_tool("shell.exec", "Run a command"),
]);
view.activate("file.read");
assert_eq!(view.activated_count(), 1);
assert!(view.is_activated("file.read"));
assert!(!view.is_activated("shell.exec"));
let active = view.activated_tools();
assert_eq!(active.len(), 1);
assert_eq!(active[0].id, "file.read");
}
#[test]
fn activate_hints_detects_dollar_prefix() {
let mut view = ProgressiveToolView::new(vec![
make_tool("file.read", "Read a file"),
make_tool("shell.exec", "Run a command"),
]);
view.activate_hints("I'll use $file.read to check the config");
assert!(view.is_activated("file.read"));
assert!(!view.is_activated("shell.exec"));
}
#[test]
fn summary_prompt_lists_all_tools() {
let mut view = ProgressiveToolView::new(vec![
make_tool("file.read", "Read a file"),
make_tool("shell.exec", "Run a command"),
]);
view.activate("file.read");
let summary = view.summary_prompt();
assert!(summary.contains("file.read"));
assert!(summary.contains("shell.exec"));
assert!(summary.contains("[active]"));
assert!(summary.contains("<tool_view>"));
}
#[test]
fn empty_tool_list_produces_empty_summary() {
let view = ProgressiveToolView::new(vec![]);
assert!(view.summary_prompt().is_empty());
}
#[test]
fn duplicate_activation_is_idempotent() {
let mut view = ProgressiveToolView::new(vec![make_tool("file.read", "Read a file")]);
view.activate("file.read");
view.activate("file.read");
assert_eq!(view.activated_count(), 1);
}
}