Skip to main content

bamboo_tools/guide/
context.rs

1//! Context for building tool usage guides.
2//!
3//! This module provides language detection and context building for generating
4//! localized tool usage guidelines that match the language of the system prompt.
5
6/// Language options for guide generation.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum GuideLanguage {
9    Chinese,
10    English,
11}
12
13impl GuideLanguage {
14    /// Detects the language from source text.
15    ///
16    /// Returns `Chinese` if CJK characters are detected, otherwise `English`.
17    pub fn detect(source: &str) -> Self {
18        if source.chars().any(is_cjk) {
19            Self::Chinese
20        } else {
21            Self::English
22        }
23    }
24}
25
26/// Build context for generating tool guides.
27///
28/// Contains language settings and configuration options for guide generation.
29#[derive(Debug, Clone)]
30pub struct GuideBuildContext {
31    /// Detected or configured language for guide content
32    pub language: GuideLanguage,
33    /// Whether to include best practices section
34    pub include_best_practices: bool,
35    /// Maximum number of examples to include per tool
36    pub max_examples_per_tool: usize,
37    /// Discoverable tools that are currently activated for the session.
38    /// Activated discoverable tools render with full guides; inactive ones
39    /// render with short summaries only.
40    pub activated_discoverable_tools: std::collections::BTreeSet<String>,
41}
42
43impl Default for GuideBuildContext {
44    fn default() -> Self {
45        Self {
46            language: GuideLanguage::English,
47            include_best_practices: true,
48            max_examples_per_tool: 2,
49            activated_discoverable_tools: std::collections::BTreeSet::new(),
50        }
51    }
52}
53
54impl GuideBuildContext {
55    /// Creates a build context by detecting language from a system prompt.
56    ///
57    /// # Arguments
58    ///
59    /// * `prompt` - The system prompt to analyze for language detection
60    pub fn from_system_prompt(prompt: &str) -> Self {
61        Self {
62            language: GuideLanguage::detect(prompt),
63            ..Self::default()
64        }
65    }
66
67    /// Returns best practices appropriate for the configured language.
68    pub fn best_practices(&self) -> &'static [&'static str] {
69        match self.language {
70            GuideLanguage::Chinese => &[
71                "Assist with defensive security tasks only; refuse offensive security requests, credential harvesting, and malware-oriented code changes.",
72                "For multi-step or non-trivial work, use Task to maintain a shared task list and keep statuses updated continuously.",
73                "Keep exactly one Task item in in_progress whenever possible; mark items completed immediately after finishing.",
74                "Read existing files before editing them. Use Edit for targeted replacements and Write only for full-file writes.",
75                "Use Glob for file discovery and Grep for content search. Do not use exec_command for find/grep/cat/head/tail/sed/awk/echo style file operations when dedicated tools exist.",
76                "For code search, narrow scope first: Glob -> Grep(files_with_matches) -> Read -> Grep(content/multiline).",
77                "Avoid broad Grep queries; keep head_limit small and add path/glob/type before multiline or content-heavy searches.",
78                "Use exec_command only for real terminal operations (build/test/git/npm/docker/etc.), not for file browsing or user-facing communication.",
79                "When multiple exec_command calls are independent, run them in parallel. For dependent commands, chain with &&.",
80                "When exec_command returns a running session_id, use write_stdin with empty chars to pull incremental output and final exit status. Choose yield_time_ms to bound each wait; completion does not send an automatic user notification. Unread completed results expire five minutes after completion, including during a diagnostic pause; handles do not survive Host restart. Poll required commands to a terminal result before finishing the task.",
81                "When polling reports sustained silence and repeated empty readouts, follow its advisory: do useful work or check one command-owned progress signal before polling again. Quiet output or an existing report does not prove completion; keep the handle, continue waiting when appropriate, and report a concrete blocker if progress cannot be established. A Human-controlled Root can pause polling after the reminder is repeatedly ignored, retaining the command handle for continuation. Do not stop/restart a command only because it is quiet.",
82                "**Parallel tool calls are strongly preferred.** The following read-only tools can safely run in parallel and SHOULD be called together in the same response whenever possible: FileExists, Glob, GetCurrentDir, GetFileInfo, Grep, Read, ViewImage, WebFetch, session_history_current, session_history. For example, if you need to read 3 files, emit all 3 Read calls in one response instead of one at a time.",
83                "For commit requests: inspect git status, git diff, and recent git log first; commit only when explicitly requested; avoid interactive git flags; use HEREDOC for multi-line commit messages.",
84                "For pull request requests: review all commits/diff since base branch, use gh to create PR with summary and test plan, and return the PR URL.",
85                "When referencing code locations in responses, use file_path:line_number format.",
86                "Use Plan to delegate planning to its read-only child and keep the root session in normal orchestration mode. Use ExitPlanMode only to leave an already-active legacy plan-mode session.",
87            ],
88            GuideLanguage::English => &[
89                "Assist with defensive security tasks only; refuse offensive security requests, credential harvesting, and malware-oriented code changes.",
90                "For multi-step or non-trivial work, use Task to maintain a shared task list and keep statuses updated continuously.",
91                "Keep exactly one Task item in in_progress whenever possible; mark items completed immediately after finishing.",
92                "Read existing files before editing them. Use Edit for targeted replacements and Write only for full-file writes.",
93                "Use Glob for file discovery and Grep for content search. Do not use exec_command for find/grep/cat/head/tail/sed/awk/echo style file operations when dedicated tools exist.",
94                "For code search, narrow scope first: Glob -> Grep(files_with_matches) -> Read -> Grep(content/multiline).",
95                "Avoid broad Grep queries; keep head_limit small and add path/glob/type before multiline or content-heavy searches.",
96                "Use exec_command only for real terminal operations (build/test/git/npm/docker/etc.), not for file browsing or user-facing communication.",
97                "When multiple exec_command calls are independent, run them in parallel. For dependent commands, chain with &&.",
98                "When exec_command returns a running session_id, use write_stdin with empty chars to pull incremental output and final exit status. Choose yield_time_ms to bound each wait; completion does not send an automatic user notification. Unread completed results expire five minutes after completion, including during a diagnostic pause; handles do not survive Host restart. Poll required commands to a terminal result before finishing the task.",
99                "When polling reports sustained silence and repeated empty readouts, follow its advisory: do useful work or check one command-owned progress signal before polling again. Quiet output or an existing report does not prove completion; keep the handle, continue waiting when appropriate, and report a concrete blocker if progress cannot be established. A Human-controlled Root can pause polling after the reminder is repeatedly ignored, retaining the command handle for continuation. Do not stop/restart a command only because it is quiet.",
100                "**Parallel tool calls are strongly preferred.** The following read-only tools can safely run in parallel and SHOULD be called together in the same response whenever possible: FileExists, Glob, GetCurrentDir, GetFileInfo, Grep, Read, ViewImage, WebFetch, session_history_current, session_history. For example, if you need to read 3 files, emit all 3 Read calls in one response instead of one at a time.",
101                "For commit requests: inspect git status, git diff, and recent git log first; commit only when explicitly requested; avoid interactive git flags; use HEREDOC for multi-line commit messages.",
102                "For pull request requests: review all commits/diff since base branch, use gh to create PR with summary and test plan, and return the PR URL.",
103                "When referencing code locations in responses, use file_path:line_number format.",
104                "Use Plan to delegate planning to its read-only child and keep the root session in normal orchestration mode. Use ExitPlanMode only to leave an already-active legacy plan-mode session.",
105            ],
106        }
107    }
108}
109
110fn is_cjk(ch: char) -> bool {
111    matches!(
112        ch,
113        '\u{3400}'..='\u{4DBF}' | '\u{4E00}'..='\u{9FFF}' | '\u{F900}'..='\u{FAFF}'
114    )
115}
116
117#[cfg(test)]
118mod tests {
119    use super::{GuideBuildContext, GuideLanguage};
120
121    #[test]
122    fn detect_language_prefers_chinese_when_cjk_present() {
123        assert_eq!(
124            GuideLanguage::detect("Please help me modify this file"),
125            GuideLanguage::English
126        );
127    }
128
129    #[test]
130    fn best_practices_prefer_delegated_plan_without_legacy_mode_switch() {
131        for language in [GuideLanguage::Chinese, GuideLanguage::English] {
132            let context = GuideBuildContext {
133                language,
134                ..GuideBuildContext::default()
135            };
136            let guidance = context.best_practices().join("\n");
137            assert!(guidance.contains("delegate planning to its read-only child"));
138            assert!(guidance.contains("already-active legacy plan-mode session"));
139        }
140    }
141
142    #[test]
143    fn best_practices_teach_model_owned_exec_polling() {
144        for language in [GuideLanguage::Chinese, GuideLanguage::English] {
145            let context = GuideBuildContext {
146                language,
147                ..GuideBuildContext::default()
148            };
149            let guidance = context.best_practices().join("\n");
150            assert!(guidance.contains("exec_command"));
151            assert!(guidance.contains("write_stdin with empty chars"));
152            assert!(guidance.contains("does not send an automatic user notification"));
153            assert!(guidance.contains("follow its advisory"));
154            assert!(guidance.contains("does not prove completion"));
155            assert!(!guidance.contains("Bash"));
156        }
157    }
158
159    #[test]
160    fn detect_language_defaults_to_english_without_cjk() {
161        assert_eq!(
162            GuideLanguage::detect("Please inspect the codebase"),
163            GuideLanguage::English
164        );
165    }
166
167    #[test]
168    fn from_system_prompt_carries_detected_language() {
169        let context = GuideBuildContext::from_system_prompt("You are a coding assistant.");
170        assert_eq!(context.language, GuideLanguage::English);
171    }
172}