use std::collections::{BTreeMap, BTreeSet};
use crate::exposure::{canonical_tool_name, is_core_tool};
use bamboo_agent_core::ToolSchema;
use serde::{Deserialize, Serialize};
pub mod builtin_guides;
pub mod context;
use builtin_guides::builtin_tool_guide;
use context::{GuideBuildContext, GuideLanguage};
use crate::tools::ToolRegistry;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolExample {
pub scenario: String,
pub parameters: serde_json::Value,
pub explanation: String,
}
impl ToolExample {
pub fn new(
scenario: impl Into<String>,
parameters: serde_json::Value,
explanation: impl Into<String>,
) -> Self {
Self {
scenario: scenario.into(),
parameters,
explanation: explanation.into(),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ToolCategory {
FileReading,
FileWriting,
CodeSearch,
CommandExecution,
GitOperations,
TaskManagement,
UserInteraction,
}
impl ToolCategory {
const ORDER: [ToolCategory; 7] = [
ToolCategory::FileReading,
ToolCategory::FileWriting,
ToolCategory::CodeSearch,
ToolCategory::CommandExecution,
ToolCategory::GitOperations,
ToolCategory::TaskManagement,
ToolCategory::UserInteraction,
];
pub fn ordered() -> &'static [ToolCategory] {
&Self::ORDER
}
fn title(self, language: GuideLanguage) -> &'static str {
match (self, language) {
(ToolCategory::FileReading, GuideLanguage::Chinese) => "File Reading Tools",
(ToolCategory::FileWriting, GuideLanguage::Chinese) => "File Writing Tools",
(ToolCategory::CodeSearch, GuideLanguage::Chinese) => "Code Search Tools",
(ToolCategory::CommandExecution, GuideLanguage::Chinese) => "Command Execution Tools",
(ToolCategory::GitOperations, GuideLanguage::Chinese) => "Git Tools",
(ToolCategory::TaskManagement, GuideLanguage::Chinese) => "Task Management Tools",
(ToolCategory::UserInteraction, GuideLanguage::Chinese) => "User Interaction Tools",
(ToolCategory::FileReading, GuideLanguage::English) => "File Reading Tools",
(ToolCategory::FileWriting, GuideLanguage::English) => "File Writing Tools",
(ToolCategory::CodeSearch, GuideLanguage::English) => "Code Search Tools",
(ToolCategory::CommandExecution, GuideLanguage::English) => "Command Tools",
(ToolCategory::GitOperations, GuideLanguage::English) => "Git Tools",
(ToolCategory::TaskManagement, GuideLanguage::English) => "Task Management Tools",
(ToolCategory::UserInteraction, GuideLanguage::English) => "User Interaction Tools",
}
}
fn description(self, language: GuideLanguage) -> &'static str {
match (self, language) {
(ToolCategory::FileReading, GuideLanguage::Chinese) => {
"Use these to understand existing files, directory structure, and metadata."
}
(ToolCategory::FileWriting, GuideLanguage::Chinese) => {
"Use these to create files or make content modifications."
}
(ToolCategory::CodeSearch, GuideLanguage::Chinese) => {
"Use these to locate definitions, references, and key text."
}
(ToolCategory::CommandExecution, GuideLanguage::Chinese) => {
"Use these to run commands, confirm or switch working directories."
}
(ToolCategory::GitOperations, GuideLanguage::Chinese) => {
"Use these to view repository status and code differences."
}
(ToolCategory::TaskManagement, GuideLanguage::Chinese) => {
"Use these to break down tasks and track execution progress."
}
(ToolCategory::UserInteraction, GuideLanguage::Chinese) => {
"Use this to confirm uncertain matters with the user."
}
(ToolCategory::FileReading, GuideLanguage::English) => {
"Use these to inspect existing files and structure."
}
(ToolCategory::FileWriting, GuideLanguage::English) => {
"Use these to create files and apply edits."
}
(ToolCategory::CodeSearch, GuideLanguage::English) => {
"Use these to find symbols, references, and patterns."
}
(ToolCategory::CommandExecution, GuideLanguage::English) => {
"Use these for shell commands and workspace context."
}
(ToolCategory::GitOperations, GuideLanguage::English) => {
"Use these to inspect repository status and diffs."
}
(ToolCategory::TaskManagement, GuideLanguage::English) => {
"Use these for planning and progress tracking."
}
(ToolCategory::UserInteraction, GuideLanguage::English) => {
"Use this when user clarification is required."
}
}
}
}
pub trait ToolGuide: Send + Sync {
fn tool_name(&self) -> &str;
fn when_to_use(&self) -> &str;
fn when_not_to_use(&self) -> &str;
fn examples(&self) -> Vec<ToolExample>;
fn related_tools(&self) -> Vec<&str>;
fn category(&self) -> ToolCategory;
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolGuideSpec {
pub tool_name: String,
pub when_to_use: String,
pub when_not_to_use: String,
pub examples: Vec<ToolExample>,
pub related_tools: Vec<String>,
pub category: ToolCategory,
}
impl ToolGuideSpec {
pub fn from_guide(guide: &dyn ToolGuide) -> Self {
Self {
tool_name: guide.tool_name().to_string(),
when_to_use: guide.when_to_use().to_string(),
when_not_to_use: guide.when_not_to_use().to_string(),
examples: guide.examples(),
related_tools: guide
.related_tools()
.into_iter()
.map(str::to_string)
.collect(),
category: guide.category(),
}
}
pub fn from_json_str(raw: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(raw)
}
pub fn from_yaml_str(raw: &str) -> Result<Self, serde_yaml::Error> {
serde_yaml::from_str(raw)
}
}
impl ToolGuide for ToolGuideSpec {
fn tool_name(&self) -> &str {
&self.tool_name
}
fn when_to_use(&self) -> &str {
&self.when_to_use
}
fn when_not_to_use(&self) -> &str {
&self.when_not_to_use
}
fn examples(&self) -> Vec<ToolExample> {
self.examples.clone()
}
fn related_tools(&self) -> Vec<&str> {
self.related_tools.iter().map(String::as_str).collect()
}
fn category(&self) -> ToolCategory {
self.category
}
}
pub struct EnhancedPromptBuilder;
impl EnhancedPromptBuilder {
pub fn build(
registry: Option<&ToolRegistry>,
available_schemas: &[ToolSchema],
context: &GuideBuildContext,
) -> String {
let mut tool_names: Vec<String> = available_schemas
.iter()
.map(|schema| schema.function.name.clone())
.collect();
tool_names.sort();
tool_names.dedup();
Self::build_for_tools(registry, &tool_names, available_schemas, context)
}
pub fn build_for_tools(
registry: Option<&ToolRegistry>,
tool_names: &[String],
fallback_schemas: &[ToolSchema],
context: &GuideBuildContext,
) -> String {
let guides = Self::collect_guides(registry, tool_names);
let activated = &context.activated_discoverable_tools;
let mut core_guides: Vec<ToolGuideSpec> = Vec::new();
let mut activated_discoverable: Vec<ToolGuideSpec> = Vec::new();
let mut inactive_discoverable: Vec<ToolGuideSpec> = Vec::new();
for guide in guides {
let canonical = canonical_tool_name(&guide.tool_name);
if is_core_tool(&canonical) {
core_guides.push(guide);
} else if activated.contains(&canonical) {
activated_discoverable.push(guide);
} else {
inactive_discoverable.push(guide);
}
}
let mut output = String::from("## Tool Usage Guidelines\n");
let mut rendered_any = false;
if !core_guides.is_empty() {
rendered_any = true;
let mut grouped: BTreeMap<ToolCategory, Vec<&ToolGuideSpec>> = BTreeMap::new();
for guide in &core_guides {
grouped.entry(guide.category).or_default().push(guide);
}
for guides in grouped.values_mut() {
guides.sort_by_key(|g| g.tool_name.clone());
}
for category in ToolCategory::ordered() {
let Some(category_guides) = grouped.get(category) else {
continue;
};
output.push_str(&format!("\n### {}\n", category.title(context.language)));
output.push_str(category.description(context.language));
output.push('\n');
for guide in category_guides {
Self::render_full_guide(&mut output, guide, context);
}
}
}
if !activated_discoverable.is_empty() {
rendered_any = true;
let mut grouped: BTreeMap<ToolCategory, Vec<&ToolGuideSpec>> = BTreeMap::new();
for guide in &activated_discoverable {
grouped.entry(guide.category).or_default().push(guide);
}
for guides in grouped.values_mut() {
guides.sort_by_key(|g| g.tool_name.clone());
}
output.push_str(&format!(
"\n### {}\n",
activated_discoverable_title(context.language)
));
output.push_str(activated_discoverable_description(context.language));
output.push('\n');
for category in ToolCategory::ordered() {
let Some(category_guides) = grouped.get(category) else {
continue;
};
output.push_str(&format!("\n#### {}\n", category.title(context.language)));
output.push_str(category.description(context.language));
output.push('\n');
for guide in category_guides {
Self::render_full_guide(&mut output, guide, context);
}
}
}
if !inactive_discoverable.is_empty() {
rendered_any = true;
output.push('\n');
output.push_str(&Self::render_discoverable_section(
&inactive_discoverable,
context,
));
}
let guided_names: BTreeSet<String> = core_guides
.iter()
.chain(activated_discoverable.iter())
.chain(inactive_discoverable.iter())
.map(|guide| guide.tool_name.clone())
.collect();
let unguided_schemas: Vec<ToolSchema> = fallback_schemas
.iter()
.filter(|schema| !guided_names.contains(schema.function.name.as_str()))
.cloned()
.collect();
if !unguided_schemas.is_empty() {
rendered_any = true;
output.push('\n');
output.push_str(&Self::render_schema_only_section(
&unguided_schemas,
context,
false,
));
}
if !rendered_any {
return Self::render_schema_only_section(fallback_schemas, context, true);
}
if context.include_best_practices {
output.push_str(&format!(
"\n### {}\n",
best_practices_title(context.language)
));
for (index, rule) in context.best_practices().iter().enumerate() {
output.push_str(&format!("{}. {}\n", index + 1, rule));
}
}
output
}
fn render_full_guide(output: &mut String, guide: &ToolGuideSpec, context: &GuideBuildContext) {
output.push_str(&format!("\n**{}**\n", guide.tool_name));
output.push_str(&format!(
"- {}: {}\n",
when_to_use_label(context.language),
guide.when_to_use
));
output.push_str(&format!(
"- {}: {}\n",
when_not_to_use_label(context.language),
guide.when_not_to_use
));
for example in guide.examples.iter().take(context.max_examples_per_tool) {
let params =
serde_json::to_string(&example.parameters).unwrap_or_else(|_| "{}".to_string());
output.push_str(&format!(
"- {}: {}\n -> {}\n",
example_label(context.language),
params,
example.explanation
));
}
if !guide.related_tools.is_empty() {
output.push_str(&format!(
"- {}: {}\n",
related_tools_label(context.language),
guide.related_tools.join(", ")
));
}
}
fn collect_guides(
registry: Option<&ToolRegistry>,
tool_names: &[String],
) -> Vec<ToolGuideSpec> {
let mut seen = BTreeSet::new();
let mut guides = Vec::new();
for raw_name in tool_names {
let name = raw_name.trim();
if name.is_empty() || !seen.insert(name.to_string()) {
continue;
}
let guide = registry
.and_then(|registry| registry.get_guide(name))
.or_else(|| builtin_tool_guide(name));
if let Some(guide) = guide {
guides.push(ToolGuideSpec::from_guide(guide.as_ref()));
}
}
guides.sort_by_key(|g| g.tool_name.clone());
guides
}
fn render_discoverable_section(
guides: &[ToolGuideSpec],
context: &GuideBuildContext,
) -> String {
if guides.is_empty() {
return String::new();
}
let mut sorted = guides.to_vec();
sorted.sort_by_key(|g| g.tool_name.clone());
let mut output = String::new();
output.push_str(&format!(
"### {}\n",
discoverable_tools_title(context.language)
));
output.push_str(discoverable_tools_description(context.language));
output.push('\n');
for guide in sorted {
output.push_str(&format!("- `{}`: {}\n", guide.tool_name, guide.when_to_use));
}
output
}
fn render_schema_only_section(
schemas: &[ToolSchema],
context: &GuideBuildContext,
include_header: bool,
) -> String {
if schemas.is_empty() {
return String::new();
}
let mut output = String::new();
if include_header {
output.push_str("## Tool Usage Guidelines\n");
}
output.push_str(&format!("\n### {}\n", schema_only_title(context.language)));
output.push_str(schema_only_description(context.language));
output.push('\n');
let mut sorted = schemas.to_vec();
sorted.sort_by_key(|s| s.function.name.clone());
for schema in sorted {
output.push_str(&format!(
"- `{}`: {}\n",
schema.function.name, schema.function.description
));
}
output
}
}
fn when_to_use_label(language: GuideLanguage) -> &'static str {
match language {
GuideLanguage::Chinese => "When to use",
GuideLanguage::English => "When to use",
}
}
fn when_not_to_use_label(language: GuideLanguage) -> &'static str {
match language {
GuideLanguage::Chinese => "When NOT to use",
GuideLanguage::English => "When NOT to use",
}
}
fn example_label(language: GuideLanguage) -> &'static str {
match language {
GuideLanguage::Chinese => "Example",
GuideLanguage::English => "Example",
}
}
fn related_tools_label(language: GuideLanguage) -> &'static str {
match language {
GuideLanguage::Chinese => "Related tools",
GuideLanguage::English => "Related tools",
}
}
fn best_practices_title(language: GuideLanguage) -> &'static str {
match language {
GuideLanguage::Chinese => "Best Practices",
GuideLanguage::English => "Best Practices",
}
}
fn discoverable_tools_title(language: GuideLanguage) -> &'static str {
match language {
GuideLanguage::Chinese => "Discoverable Tools",
GuideLanguage::English => "Discoverable Tools",
}
}
fn discoverable_tools_description(language: GuideLanguage) -> &'static str {
match language {
GuideLanguage::Chinese => {
"These lower-frequency tools are available but not fully expanded by default to save context. Activate them when needed for full parameter details and examples."
}
GuideLanguage::English => {
"These lower-frequency tools are available but not fully expanded by default to save context. Activate them when needed for full parameter details and examples."
}
}
}
fn activated_discoverable_title(language: GuideLanguage) -> &'static str {
match language {
GuideLanguage::Chinese => "Activated Discoverable Tools",
GuideLanguage::English => "Activated Discoverable Tools",
}
}
fn activated_discoverable_description(language: GuideLanguage) -> &'static str {
match language {
GuideLanguage::Chinese => {
"These discoverable tools are currently activated and available with full detail."
}
GuideLanguage::English => {
"These discoverable tools are currently activated and available with full detail."
}
}
}
fn schema_only_title(language: GuideLanguage) -> &'static str {
match language {
GuideLanguage::Chinese => "Additional Tools (Schema Only)",
GuideLanguage::English => "Additional Tools (Schema Only)",
}
}
fn schema_only_description(language: GuideLanguage) -> &'static str {
match language {
GuideLanguage::Chinese => "No detailed guide is available for these tools; rely on schema.",
GuideLanguage::English => "No detailed guide is available for these tools; rely on schema.",
}
}
#[cfg(test)]
mod tests {
use std::collections::{BTreeMap, BTreeSet};
use serde_json::json;
use crate::{
tools::{ReadTool, ToolRegistry},
BuiltinToolExecutor,
};
use bamboo_agent_core::{FunctionSchema, ToolExecutor, ToolSchema};
use super::{
context::GuideBuildContext, context::GuideLanguage, EnhancedPromptBuilder, ToolCategory,
ToolGuideSpec,
};
fn render_legacy_full_prompt(schemas: &[ToolSchema], context: &GuideBuildContext) -> String {
let tool_names: Vec<String> = schemas
.iter()
.map(|schema| schema.function.name.clone())
.collect();
let guides = EnhancedPromptBuilder::collect_guides(None, &tool_names);
if guides.is_empty() {
return EnhancedPromptBuilder::render_schema_only_section(schemas, context, true);
}
let mut output = String::from("## Tool Usage Guidelines\n");
let mut grouped: BTreeMap<ToolCategory, Vec<&ToolGuideSpec>> = BTreeMap::new();
for guide in &guides {
grouped.entry(guide.category).or_default().push(guide);
}
for guides in grouped.values_mut() {
guides.sort_by_key(|g| g.tool_name.clone());
}
for category in ToolCategory::ordered() {
let Some(category_guides) = grouped.get(category) else {
continue;
};
output.push_str(&format!("\n### {}\n", category.title(context.language)));
output.push_str(category.description(context.language));
output.push('\n');
for guide in category_guides {
output.push_str(&format!("\n**{}**\n", guide.tool_name));
output.push_str(&format!(
"- {}: {}\n",
super::when_to_use_label(context.language),
guide.when_to_use
));
output.push_str(&format!(
"- {}: {}\n",
super::when_not_to_use_label(context.language),
guide.when_not_to_use
));
for example in guide.examples.iter().take(context.max_examples_per_tool) {
let params = serde_json::to_string(&example.parameters)
.unwrap_or_else(|_| "{}".to_string());
output.push_str(&format!(
"- {}: {}\n -> {}\n",
super::example_label(context.language),
params,
example.explanation
));
}
if !guide.related_tools.is_empty() {
output.push_str(&format!(
"- {}: {}\n",
super::related_tools_label(context.language),
guide.related_tools.join(", ")
));
}
}
}
let guided_names: BTreeSet<&str> = guides
.iter()
.map(|guide| guide.tool_name.as_str())
.collect();
let unguided_schemas: Vec<ToolSchema> = schemas
.iter()
.filter(|schema| !guided_names.contains(schema.function.name.as_str()))
.cloned()
.collect();
if !unguided_schemas.is_empty() {
output.push('\n');
output.push_str(&EnhancedPromptBuilder::render_schema_only_section(
&unguided_schemas,
context,
false,
));
}
if context.include_best_practices {
output.push_str(&format!(
"\n### {}\n",
super::best_practices_title(context.language)
));
for (index, rule) in context.best_practices().iter().enumerate() {
output.push_str(&format!("{}. {}\n", index + 1, rule));
}
}
output
}
#[test]
fn build_renders_builtin_guides() {
let registry = ToolRegistry::new();
registry.register(ReadTool::new()).unwrap();
let schemas = registry.list_tools();
let prompt =
EnhancedPromptBuilder::build(Some(®istry), &schemas, &GuideBuildContext::default());
assert!(prompt.contains("## Tool Usage Guidelines"));
assert!(prompt.contains("**Read**"));
}
#[test]
fn build_falls_back_to_schema_without_guides() {
let schema = ToolSchema {
schema_type: "function".to_string(),
function: FunctionSchema {
name: "dynamic_tool".to_string(),
description: "A runtime tool".to_string(),
parameters: json!({ "type": "object", "properties": {} }),
},
};
let context = GuideBuildContext {
language: GuideLanguage::English,
..GuideBuildContext::default()
};
let prompt = EnhancedPromptBuilder::build(None, &[schema], &context);
assert!(prompt.contains("Additional Tools (Schema Only)"));
assert!(prompt.contains("dynamic_tool"));
}
#[test]
fn build_summarizes_discoverable_tools() {
let registry = ToolRegistry::new();
registry.register(ReadTool::new()).unwrap();
registry.register(crate::tools::SleepTool::new()).unwrap();
let schemas = registry.list_tools();
let prompt =
EnhancedPromptBuilder::build(Some(®istry), &schemas, &GuideBuildContext::default());
assert!(prompt.contains("### Discoverable Tools"));
assert!(prompt.contains("`Sleep`"));
assert!(!prompt.contains("**Sleep**"));
}
#[test]
fn build_reduces_prompt_length_vs_legacy_full_guides_for_builtin_surface() {
let executor = BuiltinToolExecutor::new();
let schemas = executor.list_tools();
let context = GuideBuildContext::default();
let legacy = render_legacy_full_prompt(&schemas, &context);
let current = EnhancedPromptBuilder::build(None, &schemas, &context);
assert!(current.len() < legacy.len());
let saved = legacy.len() - current.len();
let saved_ratio = saved as f64 / legacy.len() as f64;
eprintln!(
"guide_length_metrics: legacy={}, current={}, saved={}, saved_ratio={:.3}",
legacy.len(),
current.len(),
saved,
saved_ratio,
);
assert!(
saved > 0,
"expected prompt savings for summarized discoverable tools"
);
}
#[test]
fn build_shows_activated_discoverable_tools_with_full_detail() {
let registry = ToolRegistry::new();
registry.register(crate::tools::SleepTool::new()).unwrap();
let schemas = registry.list_tools();
let mut context = GuideBuildContext::default();
context
.activated_discoverable_tools
.insert("Sleep".to_string());
let prompt = EnhancedPromptBuilder::build(Some(®istry), &schemas, &context);
assert!(
prompt.contains("### Activated Discoverable Tools"),
"activated discoverable section should appear"
);
assert!(
prompt.contains("**Sleep**"),
"activated Sleep should show full guide with bold name"
);
assert!(
prompt.contains("When to use"),
"activated Sleep should include when_to_use"
);
assert!(
prompt.contains("When NOT to use"),
"activated Sleep should include when_not_to_use"
);
assert!(
!prompt.contains("### Discoverable Tools"),
"inactive discoverable section should not appear when all discoverable tools are activated"
);
}
#[test]
fn build_shows_inactive_discoverable_tools_with_short_summary() {
let registry = ToolRegistry::new();
registry.register(crate::tools::SleepTool::new()).unwrap();
let schemas = registry.list_tools();
let context = GuideBuildContext::default();
let prompt = EnhancedPromptBuilder::build(Some(®istry), &schemas, &context);
assert!(
prompt.contains("### Discoverable Tools"),
"inactive discoverable section should appear"
);
assert!(
prompt.contains("`Sleep`"),
"inactive Sleep should show as short summary"
);
assert!(
!prompt.contains("**Sleep**"),
"inactive Sleep should NOT show full guide with bold name"
);
assert!(
!prompt.contains("Activated Discoverable Tools"),
"activated section should not appear when no discoverable tools are activated"
);
}
#[test]
fn build_separates_core_and_discoverable_tools_correctly() {
let registry = ToolRegistry::new();
registry.register(ReadTool::new()).unwrap();
registry.register(crate::tools::SleepTool::new()).unwrap();
let schemas = registry.list_tools();
let mut context = GuideBuildContext::default();
context
.activated_discoverable_tools
.insert("Sleep".to_string());
let prompt = EnhancedPromptBuilder::build(Some(®istry), &schemas, &context);
assert!(
prompt.contains("### File Reading Tools"),
"core tools should appear in category section"
);
assert!(
prompt.contains("**Read**"),
"core Read should show full guide"
);
assert!(
prompt.contains("### Activated Discoverable Tools"),
"activated discoverable section should appear"
);
assert!(
prompt.contains("**Sleep**"),
"activated Sleep should show full guide"
);
}
}