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 listed here only in brief to save context. Their full parameter details and examples become visible once they are activated for this session (activation is handled by the app or user, not via a tool call)."
703 }
704 GuideLanguage::English => {
705 "These lower-frequency tools are available but listed here only in brief to save context. Their full parameter details and examples become visible once they are activated for this session (activation is handled by the app or user, not via a tool call)."
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}