1use std::collections::{BTreeMap, BTreeSet};
30
31use crate::exposure::{canonical_tool_name, is_core_tool};
32use bamboo_agent_core::ToolSchema;
33use serde::{Deserialize, Serialize};
34
35pub mod builtin_guides;
36pub mod context;
37
38use builtin_guides::builtin_tool_guide;
39use context::{GuideBuildContext, GuideLanguage};
40
41use crate::tools::ToolRegistry;
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48pub struct ToolExample {
49 pub scenario: String,
51
52 pub parameters: serde_json::Value,
54
55 pub explanation: String,
57}
58
59impl ToolExample {
60 pub fn new(
68 scenario: impl Into<String>,
69 parameters: serde_json::Value,
70 explanation: impl Into<String>,
71 ) -> Self {
72 Self {
73 scenario: scenario.into(),
74 parameters,
75 explanation: explanation.into(),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
85pub enum ToolCategory {
86 FileReading,
88
89 FileWriting,
91
92 CodeSearch,
94
95 CommandExecution,
97
98 GitOperations,
100
101 TaskManagement,
103
104 UserInteraction,
106}
107
108impl ToolCategory {
109 const ORDER: [ToolCategory; 7] = [
111 ToolCategory::FileReading,
112 ToolCategory::FileWriting,
113 ToolCategory::CodeSearch,
114 ToolCategory::CommandExecution,
115 ToolCategory::GitOperations,
116 ToolCategory::TaskManagement,
117 ToolCategory::UserInteraction,
118 ];
119
120 pub fn ordered() -> &'static [ToolCategory] {
122 &Self::ORDER
123 }
124
125 fn title(self, language: GuideLanguage) -> &'static str {
127 match (self, language) {
128 (ToolCategory::FileReading, GuideLanguage::Chinese) => "File Reading Tools",
129 (ToolCategory::FileWriting, GuideLanguage::Chinese) => "File Writing Tools",
130 (ToolCategory::CodeSearch, GuideLanguage::Chinese) => "Code Search Tools",
131 (ToolCategory::CommandExecution, GuideLanguage::Chinese) => "Command Execution Tools",
132 (ToolCategory::GitOperations, GuideLanguage::Chinese) => "Git Tools",
133 (ToolCategory::TaskManagement, GuideLanguage::Chinese) => "Task Management Tools",
134 (ToolCategory::UserInteraction, GuideLanguage::Chinese) => "User Interaction Tools",
135 (ToolCategory::FileReading, GuideLanguage::English) => "File Reading Tools",
136 (ToolCategory::FileWriting, GuideLanguage::English) => "File Writing Tools",
137 (ToolCategory::CodeSearch, GuideLanguage::English) => "Code Search Tools",
138 (ToolCategory::CommandExecution, GuideLanguage::English) => "Command Tools",
139 (ToolCategory::GitOperations, GuideLanguage::English) => "Git Tools",
140 (ToolCategory::TaskManagement, GuideLanguage::English) => "Task Management Tools",
141 (ToolCategory::UserInteraction, GuideLanguage::English) => "User Interaction Tools",
142 }
143 }
144
145 fn description(self, language: GuideLanguage) -> &'static str {
147 match (self, language) {
148 (ToolCategory::FileReading, GuideLanguage::Chinese) => {
149 "Use these to understand existing files, directory structure, and metadata."
150 }
151 (ToolCategory::FileWriting, GuideLanguage::Chinese) => {
152 "Use these to create files or make content modifications."
153 }
154 (ToolCategory::CodeSearch, GuideLanguage::Chinese) => {
155 "Use these to locate definitions, references, and key text."
156 }
157 (ToolCategory::CommandExecution, GuideLanguage::Chinese) => {
158 "Use these to run commands, confirm or switch working directories."
159 }
160 (ToolCategory::GitOperations, GuideLanguage::Chinese) => {
161 "Use these to view repository status and code differences."
162 }
163 (ToolCategory::TaskManagement, GuideLanguage::Chinese) => {
164 "Use these to break down tasks and track execution progress."
165 }
166 (ToolCategory::UserInteraction, GuideLanguage::Chinese) => {
167 "Use this to confirm uncertain matters with the user."
168 }
169 (ToolCategory::FileReading, GuideLanguage::English) => {
170 "Use these to inspect existing files and structure."
171 }
172 (ToolCategory::FileWriting, GuideLanguage::English) => {
173 "Use these to create files and apply edits."
174 }
175 (ToolCategory::CodeSearch, GuideLanguage::English) => {
176 "Use these to find symbols, references, and patterns."
177 }
178 (ToolCategory::CommandExecution, GuideLanguage::English) => {
179 "Use these for shell commands and workspace context."
180 }
181 (ToolCategory::GitOperations, GuideLanguage::English) => {
182 "Use these to inspect repository status and diffs."
183 }
184 (ToolCategory::TaskManagement, GuideLanguage::English) => {
185 "Use these for planning and progress tracking."
186 }
187 (ToolCategory::UserInteraction, GuideLanguage::English) => {
188 "Use this when user clarification is required."
189 }
190 }
191 }
192}
193
194pub trait ToolGuide: Send + Sync {
208 fn tool_name(&self) -> &str;
210
211 fn when_to_use(&self) -> &str;
213
214 fn when_not_to_use(&self) -> &str;
216
217 fn examples(&self) -> Vec<ToolExample>;
219
220 fn related_tools(&self) -> Vec<&str>;
222
223 fn category(&self) -> ToolCategory;
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
232pub struct ToolGuideSpec {
233 pub tool_name: String,
235
236 pub when_to_use: String,
238
239 pub when_not_to_use: String,
241
242 pub examples: Vec<ToolExample>,
244
245 pub related_tools: Vec<String>,
247
248 pub category: ToolCategory,
250}
251
252impl ToolGuideSpec {
253 pub fn from_guide(guide: &dyn ToolGuide) -> Self {
259 Self {
260 tool_name: guide.tool_name().to_string(),
261 when_to_use: guide.when_to_use().to_string(),
262 when_not_to_use: guide.when_not_to_use().to_string(),
263 examples: guide.examples(),
264 related_tools: guide
265 .related_tools()
266 .into_iter()
267 .map(str::to_string)
268 .collect(),
269 category: guide.category(),
270 }
271 }
272
273 pub fn from_json_str(raw: &str) -> Result<Self, serde_json::Error> {
279 serde_json::from_str(raw)
280 }
281
282 pub fn from_yaml_str(raw: &str) -> Result<Self, serde_yaml::Error> {
288 serde_yaml::from_str(raw)
289 }
290}
291
292impl ToolGuide for ToolGuideSpec {
293 fn tool_name(&self) -> &str {
294 &self.tool_name
295 }
296
297 fn when_to_use(&self) -> &str {
298 &self.when_to_use
299 }
300
301 fn when_not_to_use(&self) -> &str {
302 &self.when_not_to_use
303 }
304
305 fn examples(&self) -> Vec<ToolExample> {
306 self.examples.clone()
307 }
308
309 fn related_tools(&self) -> Vec<&str> {
310 self.related_tools.iter().map(String::as_str).collect()
311 }
312
313 fn category(&self) -> ToolCategory {
314 self.category
315 }
316}
317
318pub struct EnhancedPromptBuilder;
346
347impl EnhancedPromptBuilder {
348 pub fn build(
363 registry: Option<&ToolRegistry>,
364 available_schemas: &[ToolSchema],
365 context: &GuideBuildContext,
366 ) -> String {
367 let mut tool_names: Vec<String> = available_schemas
368 .iter()
369 .map(|schema| schema.function.name.clone())
370 .collect();
371 tool_names.sort();
372 tool_names.dedup();
373
374 Self::build_for_tools(registry, &tool_names, available_schemas, context)
375 }
376
377 pub fn build_for_tools(
393 registry: Option<&ToolRegistry>,
394 tool_names: &[String],
395 fallback_schemas: &[ToolSchema],
396 context: &GuideBuildContext,
397 ) -> String {
398 let guides = Self::collect_guides(registry, tool_names);
399
400 let activated = &context.activated_discoverable_tools;
402 let mut core_guides: Vec<ToolGuideSpec> = Vec::new();
403 let mut activated_discoverable: Vec<ToolGuideSpec> = Vec::new();
404 let mut inactive_discoverable: Vec<ToolGuideSpec> = Vec::new();
405
406 for guide in guides {
407 let canonical = canonical_tool_name(&guide.tool_name);
408 if is_core_tool(&canonical) {
409 core_guides.push(guide);
410 } else if activated.contains(&canonical) {
411 activated_discoverable.push(guide);
412 } else {
413 inactive_discoverable.push(guide);
414 }
415 }
416
417 let mut output = String::from("## Tool Usage Guidelines\n");
418 let mut rendered_any = false;
419
420 if !core_guides.is_empty() {
422 rendered_any = true;
423 let mut grouped: BTreeMap<ToolCategory, Vec<&ToolGuideSpec>> = BTreeMap::new();
424 for guide in &core_guides {
425 grouped.entry(guide.category).or_default().push(guide);
426 }
427
428 for guides in grouped.values_mut() {
429 guides.sort_by_key(|g| g.tool_name.clone());
430 }
431
432 for category in ToolCategory::ordered() {
433 let Some(category_guides) = grouped.get(category) else {
434 continue;
435 };
436
437 output.push_str(&format!("\n### {}\n", category.title(context.language)));
438 output.push_str(category.description(context.language));
439 output.push('\n');
440
441 for guide in category_guides {
442 Self::render_full_guide(&mut output, guide, context);
443 }
444 }
445 }
446
447 if !activated_discoverable.is_empty() {
449 rendered_any = true;
450 let mut grouped: BTreeMap<ToolCategory, Vec<&ToolGuideSpec>> = BTreeMap::new();
451 for guide in &activated_discoverable {
452 grouped.entry(guide.category).or_default().push(guide);
453 }
454
455 for guides in grouped.values_mut() {
456 guides.sort_by_key(|g| g.tool_name.clone());
457 }
458
459 output.push_str(&format!(
460 "\n### {}\n",
461 activated_discoverable_title(context.language)
462 ));
463 output.push_str(activated_discoverable_description(context.language));
464 output.push('\n');
465
466 for category in ToolCategory::ordered() {
467 let Some(category_guides) = grouped.get(category) else {
468 continue;
469 };
470
471 output.push_str(&format!("\n#### {}\n", category.title(context.language)));
472 output.push_str(category.description(context.language));
473 output.push('\n');
474
475 for guide in category_guides {
476 Self::render_full_guide(&mut output, guide, context);
477 }
478 }
479 }
480
481 if !inactive_discoverable.is_empty() {
483 rendered_any = true;
484 output.push('\n');
485 output.push_str(&Self::render_discoverable_section(
486 &inactive_discoverable,
487 context,
488 ));
489 }
490
491 let guided_names: BTreeSet<String> = core_guides
492 .iter()
493 .chain(activated_discoverable.iter())
494 .chain(inactive_discoverable.iter())
495 .map(|guide| guide.tool_name.clone())
496 .collect();
497 let unguided_schemas: Vec<ToolSchema> = fallback_schemas
498 .iter()
499 .filter(|schema| !guided_names.contains(schema.function.name.as_str()))
500 .cloned()
501 .collect();
502
503 if !unguided_schemas.is_empty() {
504 rendered_any = true;
505 output.push('\n');
506 output.push_str(&Self::render_schema_only_section(
507 &unguided_schemas,
508 context,
509 false,
510 ));
511 }
512
513 if !rendered_any {
514 return Self::render_schema_only_section(fallback_schemas, context, true);
515 }
516
517 if context.include_best_practices {
518 output.push_str(&format!(
519 "\n### {}\n",
520 best_practices_title(context.language)
521 ));
522 for (index, rule) in context.best_practices().iter().enumerate() {
523 output.push_str(&format!("{}. {}\n", index + 1, rule));
524 }
525 }
526
527 output
528 }
529
530 fn render_full_guide(output: &mut String, guide: &ToolGuideSpec, context: &GuideBuildContext) {
531 output.push_str(&format!("\n**{}**\n", guide.tool_name));
532 output.push_str(&format!(
533 "- {}: {}\n",
534 when_to_use_label(context.language),
535 guide.when_to_use
536 ));
537 output.push_str(&format!(
538 "- {}: {}\n",
539 when_not_to_use_label(context.language),
540 guide.when_not_to_use
541 ));
542
543 for example in guide.examples.iter().take(context.max_examples_per_tool) {
544 let params =
545 serde_json::to_string(&example.parameters).unwrap_or_else(|_| "{}".to_string());
546 output.push_str(&format!(
547 "- {}: {}\n -> {}\n",
548 example_label(context.language),
549 params,
550 example.explanation
551 ));
552 }
553
554 if !guide.related_tools.is_empty() {
555 output.push_str(&format!(
556 "- {}: {}\n",
557 related_tools_label(context.language),
558 guide.related_tools.join(", ")
559 ));
560 }
561 }
562
563 fn collect_guides(
565 registry: Option<&ToolRegistry>,
566 tool_names: &[String],
567 ) -> Vec<ToolGuideSpec> {
568 let mut seen = BTreeSet::new();
569 let mut guides = Vec::new();
570
571 for raw_name in tool_names {
572 let name = raw_name.trim();
573 if name.is_empty() || !seen.insert(name.to_string()) {
574 continue;
575 }
576
577 let guide = registry
578 .and_then(|registry| registry.get_guide(name))
579 .or_else(|| builtin_tool_guide(name));
580
581 if let Some(guide) = guide {
582 guides.push(ToolGuideSpec::from_guide(guide.as_ref()));
583 }
584 }
585
586 guides.sort_by_key(|g| g.tool_name.clone());
587 guides
588 }
589
590 fn render_discoverable_section(
592 guides: &[ToolGuideSpec],
593 context: &GuideBuildContext,
594 ) -> String {
595 if guides.is_empty() {
596 return String::new();
597 }
598
599 let mut sorted = guides.to_vec();
600 sorted.sort_by_key(|g| g.tool_name.clone());
601
602 let mut output = String::new();
603 output.push_str(&format!(
604 "### {}\n",
605 discoverable_tools_title(context.language)
606 ));
607 output.push_str(discoverable_tools_description(context.language));
608 output.push('\n');
609 for guide in sorted {
610 output.push_str(&format!("- `{}`: {}\n", guide.tool_name, guide.when_to_use));
611 }
612 output
613 }
614
615 fn render_schema_only_section(
617 schemas: &[ToolSchema],
618 context: &GuideBuildContext,
619 include_header: bool,
620 ) -> String {
621 if schemas.is_empty() {
622 return String::new();
623 }
624
625 let mut output = String::new();
626 if include_header {
627 output.push_str("## Tool Usage Guidelines\n");
628 }
629
630 output.push_str(&format!("\n### {}\n", schema_only_title(context.language)));
631 output.push_str(schema_only_description(context.language));
632 output.push('\n');
633
634 let mut sorted = schemas.to_vec();
635 sorted.sort_by_key(|s| s.function.name.clone());
636
637 for schema in sorted {
638 output.push_str(&format!(
639 "- `{}`: {}\n",
640 schema.function.name, schema.function.description
641 ));
642 }
643
644 output
645 }
646}
647
648fn when_to_use_label(language: GuideLanguage) -> &'static str {
652 match language {
653 GuideLanguage::Chinese => "When to use",
654 GuideLanguage::English => "When to use",
655 }
656}
657
658fn when_not_to_use_label(language: GuideLanguage) -> &'static str {
660 match language {
661 GuideLanguage::Chinese => "When NOT to use",
662 GuideLanguage::English => "When NOT to use",
663 }
664}
665
666fn example_label(language: GuideLanguage) -> &'static str {
668 match language {
669 GuideLanguage::Chinese => "Example",
670 GuideLanguage::English => "Example",
671 }
672}
673
674fn related_tools_label(language: GuideLanguage) -> &'static str {
676 match language {
677 GuideLanguage::Chinese => "Related tools",
678 GuideLanguage::English => "Related tools",
679 }
680}
681
682fn best_practices_title(language: GuideLanguage) -> &'static str {
684 match language {
685 GuideLanguage::Chinese => "Best Practices",
686 GuideLanguage::English => "Best Practices",
687 }
688}
689
690fn discoverable_tools_title(language: GuideLanguage) -> &'static str {
692 match language {
693 GuideLanguage::Chinese => "Discoverable Tools",
694 GuideLanguage::English => "Discoverable Tools",
695 }
696}
697
698fn discoverable_tools_description(language: GuideLanguage) -> &'static str {
700 match language {
701 GuideLanguage::Chinese => {
702 "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."
703 }
704 GuideLanguage::English => {
705 "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."
706 }
707 }
708}
709
710fn activated_discoverable_title(language: GuideLanguage) -> &'static str {
712 match language {
713 GuideLanguage::Chinese => "Activated Discoverable Tools",
714 GuideLanguage::English => "Activated Discoverable Tools",
715 }
716}
717
718fn activated_discoverable_description(language: GuideLanguage) -> &'static str {
720 match language {
721 GuideLanguage::Chinese => {
722 "These discoverable tools are currently activated and available with full detail."
723 }
724 GuideLanguage::English => {
725 "These discoverable tools are currently activated and available with full detail."
726 }
727 }
728}
729
730fn schema_only_title(language: GuideLanguage) -> &'static str {
732 match language {
733 GuideLanguage::Chinese => "Additional Tools (Schema Only)",
734 GuideLanguage::English => "Additional Tools (Schema Only)",
735 }
736}
737
738fn schema_only_description(language: GuideLanguage) -> &'static str {
740 match language {
741 GuideLanguage::Chinese => "No detailed guide is available for these tools; rely on schema.",
742 GuideLanguage::English => "No detailed guide is available for these tools; rely on schema.",
743 }
744}
745
746#[cfg(test)]
747mod tests {
748 use std::collections::{BTreeMap, BTreeSet};
749
750 use serde_json::json;
751
752 use crate::{
753 tools::{ReadTool, ToolRegistry},
754 BuiltinToolExecutor,
755 };
756 use bamboo_agent_core::{FunctionSchema, ToolExecutor, ToolSchema};
757
758 use super::{
759 context::GuideBuildContext, context::GuideLanguage, EnhancedPromptBuilder, ToolCategory,
760 ToolGuideSpec,
761 };
762
763 fn render_legacy_full_prompt(schemas: &[ToolSchema], context: &GuideBuildContext) -> String {
764 let tool_names: Vec<String> = schemas
765 .iter()
766 .map(|schema| schema.function.name.clone())
767 .collect();
768 let guides = EnhancedPromptBuilder::collect_guides(None, &tool_names);
769
770 if guides.is_empty() {
771 return EnhancedPromptBuilder::render_schema_only_section(schemas, context, true);
772 }
773
774 let mut output = String::from("## Tool Usage Guidelines\n");
775 let mut grouped: BTreeMap<ToolCategory, Vec<&ToolGuideSpec>> = BTreeMap::new();
776
777 for guide in &guides {
778 grouped.entry(guide.category).or_default().push(guide);
779 }
780
781 for guides in grouped.values_mut() {
782 guides.sort_by_key(|g| g.tool_name.clone());
783 }
784
785 for category in ToolCategory::ordered() {
786 let Some(category_guides) = grouped.get(category) else {
787 continue;
788 };
789
790 output.push_str(&format!("\n### {}\n", category.title(context.language)));
791 output.push_str(category.description(context.language));
792 output.push('\n');
793
794 for guide in category_guides {
795 output.push_str(&format!("\n**{}**\n", guide.tool_name));
796 output.push_str(&format!(
797 "- {}: {}\n",
798 super::when_to_use_label(context.language),
799 guide.when_to_use
800 ));
801 output.push_str(&format!(
802 "- {}: {}\n",
803 super::when_not_to_use_label(context.language),
804 guide.when_not_to_use
805 ));
806
807 for example in guide.examples.iter().take(context.max_examples_per_tool) {
808 let params = serde_json::to_string(&example.parameters)
809 .unwrap_or_else(|_| "{}".to_string());
810 output.push_str(&format!(
811 "- {}: {}\n -> {}\n",
812 super::example_label(context.language),
813 params,
814 example.explanation
815 ));
816 }
817
818 if !guide.related_tools.is_empty() {
819 output.push_str(&format!(
820 "- {}: {}\n",
821 super::related_tools_label(context.language),
822 guide.related_tools.join(", ")
823 ));
824 }
825 }
826 }
827
828 let guided_names: BTreeSet<&str> = guides
829 .iter()
830 .map(|guide| guide.tool_name.as_str())
831 .collect();
832 let unguided_schemas: Vec<ToolSchema> = schemas
833 .iter()
834 .filter(|schema| !guided_names.contains(schema.function.name.as_str()))
835 .cloned()
836 .collect();
837
838 if !unguided_schemas.is_empty() {
839 output.push('\n');
840 output.push_str(&EnhancedPromptBuilder::render_schema_only_section(
841 &unguided_schemas,
842 context,
843 false,
844 ));
845 }
846
847 if context.include_best_practices {
848 output.push_str(&format!(
849 "\n### {}\n",
850 super::best_practices_title(context.language)
851 ));
852 for (index, rule) in context.best_practices().iter().enumerate() {
853 output.push_str(&format!("{}. {}\n", index + 1, rule));
854 }
855 }
856
857 output
858 }
859
860 #[test]
861 fn build_renders_builtin_guides() {
862 let registry = ToolRegistry::new();
863 registry.register(ReadTool::new()).unwrap();
864
865 let schemas = registry.list_tools();
866 let prompt =
867 EnhancedPromptBuilder::build(Some(®istry), &schemas, &GuideBuildContext::default());
868
869 assert!(prompt.contains("## Tool Usage Guidelines"));
870 assert!(prompt.contains("**Read**"));
871 }
872
873 #[test]
874 fn build_falls_back_to_schema_without_guides() {
875 let schema = ToolSchema {
876 schema_type: "function".to_string(),
877 function: FunctionSchema {
878 name: "dynamic_tool".to_string(),
879 description: "A runtime tool".to_string(),
880 parameters: json!({ "type": "object", "properties": {} }),
881 },
882 };
883 let context = GuideBuildContext {
884 language: GuideLanguage::English,
885 ..GuideBuildContext::default()
886 };
887
888 let prompt = EnhancedPromptBuilder::build(None, &[schema], &context);
889
890 assert!(prompt.contains("Additional Tools (Schema Only)"));
891 assert!(prompt.contains("dynamic_tool"));
892 }
893
894 #[test]
895 fn build_summarizes_discoverable_tools() {
896 let registry = ToolRegistry::new();
897 registry.register(ReadTool::new()).unwrap();
898 registry.register(crate::tools::SleepTool::new()).unwrap();
899
900 let schemas = registry.list_tools();
901 let prompt =
902 EnhancedPromptBuilder::build(Some(®istry), &schemas, &GuideBuildContext::default());
903
904 assert!(prompt.contains("### Discoverable Tools"));
905 assert!(prompt.contains("`Sleep`"));
906 assert!(!prompt.contains("**Sleep**"));
907 }
908
909 #[test]
910 fn build_reduces_prompt_length_vs_legacy_full_guides_for_builtin_surface() {
911 let executor = BuiltinToolExecutor::new();
912 let schemas = executor.list_tools();
913 let context = GuideBuildContext::default();
914
915 let legacy = render_legacy_full_prompt(&schemas, &context);
916 let current = EnhancedPromptBuilder::build(None, &schemas, &context);
917
918 assert!(current.len() < legacy.len());
919 let saved = legacy.len() - current.len();
920 let saved_ratio = saved as f64 / legacy.len() as f64;
921 eprintln!(
922 "guide_length_metrics: legacy={}, current={}, saved={}, saved_ratio={:.3}",
923 legacy.len(),
924 current.len(),
925 saved,
926 saved_ratio,
927 );
928 assert!(
929 saved > 0,
930 "expected prompt savings for summarized discoverable tools"
931 );
932 }
933
934 #[test]
935 fn build_shows_activated_discoverable_tools_with_full_detail() {
936 let registry = ToolRegistry::new();
937 registry.register(crate::tools::SleepTool::new()).unwrap();
938
939 let schemas = registry.list_tools();
940 let mut context = GuideBuildContext::default();
941 context
942 .activated_discoverable_tools
943 .insert("Sleep".to_string());
944
945 let prompt = EnhancedPromptBuilder::build(Some(®istry), &schemas, &context);
946
947 assert!(
948 prompt.contains("### Activated Discoverable Tools"),
949 "activated discoverable section should appear"
950 );
951 assert!(
952 prompt.contains("**Sleep**"),
953 "activated Sleep should show full guide with bold name"
954 );
955 assert!(
956 prompt.contains("When to use"),
957 "activated Sleep should include when_to_use"
958 );
959 assert!(
960 prompt.contains("When NOT to use"),
961 "activated Sleep should include when_not_to_use"
962 );
963 assert!(
964 !prompt.contains("### Discoverable Tools"),
965 "inactive discoverable section should not appear when all discoverable tools are activated"
966 );
967 }
968
969 #[test]
970 fn build_shows_inactive_discoverable_tools_with_short_summary() {
971 let registry = ToolRegistry::new();
972 registry.register(crate::tools::SleepTool::new()).unwrap();
973
974 let schemas = registry.list_tools();
975 let context = GuideBuildContext::default();
976
977 let prompt = EnhancedPromptBuilder::build(Some(®istry), &schemas, &context);
978
979 assert!(
980 prompt.contains("### Discoverable Tools"),
981 "inactive discoverable section should appear"
982 );
983 assert!(
984 prompt.contains("`Sleep`"),
985 "inactive Sleep should show as short summary"
986 );
987 assert!(
988 !prompt.contains("**Sleep**"),
989 "inactive Sleep should NOT show full guide with bold name"
990 );
991 assert!(
992 !prompt.contains("Activated Discoverable Tools"),
993 "activated section should not appear when no discoverable tools are activated"
994 );
995 }
996
997 #[test]
998 fn build_separates_core_and_discoverable_tools_correctly() {
999 let registry = ToolRegistry::new();
1000 registry.register(ReadTool::new()).unwrap();
1001 registry.register(crate::tools::SleepTool::new()).unwrap();
1002
1003 let schemas = registry.list_tools();
1004 let mut context = GuideBuildContext::default();
1005 context
1006 .activated_discoverable_tools
1007 .insert("Sleep".to_string());
1008
1009 let prompt = EnhancedPromptBuilder::build(Some(®istry), &schemas, &context);
1010
1011 assert!(
1013 prompt.contains("### File Reading Tools"),
1014 "core tools should appear in category section"
1015 );
1016 assert!(
1017 prompt.contains("**Read**"),
1018 "core Read should show full guide"
1019 );
1020
1021 assert!(
1023 prompt.contains("### Activated Discoverable Tools"),
1024 "activated discoverable section should appear"
1025 );
1026 assert!(
1027 prompt.contains("**Sleep**"),
1028 "activated Sleep should show full guide"
1029 );
1030 }
1031}