From c7a381d5e8715ec46d2fa15fd98433d60d38d0bc Mon Sep 17 00:00:00 2001 From: LING71671 <1739677116@qq.com> Date: Wed, 27 May 2026 23:04:26 +0800 Subject: [PATCH 1/2] feat(prompt): add core tool taxonomy block --- crates/tui/src/core/engine.rs | 4 ++ crates/tui/src/prompts.rs | 80 ++++++++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index 02737eb7a..558a809e0 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -2085,6 +2085,10 @@ mod tool_execution; mod tool_setup; mod turn_loop; +pub(crate) fn default_active_native_tool_names() -> &'static [&'static str] { + tool_catalog::DEFAULT_ACTIVE_NATIVE_TOOLS +} + use self::approval::{ApprovalDecision, ApprovalResult, UserInputDecision}; #[cfg(test)] use self::dispatch::should_parallelize_tool_batch; diff --git a/crates/tui/src/prompts.rs b/crates/tui/src/prompts.rs index c43f2e846..5e01ea927 100644 --- a/crates/tui/src/prompts.rs +++ b/crates/tui/src/prompts.rs @@ -2,7 +2,7 @@ //! System prompts for different modes. //! //! Prompts are assembled from composable layers loaded at compile time: -//! base.md → personality overlay → mode delta → approval policy +//! tool taxonomy → base.md → personality overlay → mode delta → approval policy //! //! This keeps each concern in its own file and makes prompt tuning //! a single-file operation. @@ -490,10 +490,11 @@ fn approval_prompt_for_mode(mode: AppMode, approval_mode: ApprovalMode) -> &'sta } /// Compose the full system prompt in deterministic order: -/// 1. base.md — core identity, toolbox, execution contract -/// 2. personality — voice and tone overlay -/// 3. mode delta — mode-specific permissions and workflow -/// 4. approval policy — tool-approval behavior +/// 1. tool taxonomy — compact hints generated from the eager core tools +/// 2. base.md — core identity, toolbox, execution contract +/// 3. personality — voice and tone overlay +/// 4. mode delta — mode-specific permissions and workflow +/// 5. approval policy — tool-approval behavior /// /// Each layer is separated by a blank line for readability in the /// rendered prompt (the model sees them as contiguous sections). @@ -506,6 +507,30 @@ fn apply_model_template(prompt: &str, model_id: &str) -> String { prompt.replace("{model_id}", model_id) } +const TOOL_TAXONOMY_DISCOVERY: &[&str] = &["grep_files", "file_search"]; +const TOOL_TAXONOMY_GIT: &[&str] = &["git_status", "git_diff"]; +const TOOL_TAXONOMY_VERIFICATION: &[&str] = &["run_tests"]; + +fn render_core_tool_taxonomy_block() -> String { + let core_tools = crate::core::engine::default_active_native_tool_names(); + format!( + "## Core Tool Taxonomy\n\nUse {} for discovery. Use {} for git inspection. Use {} for verification.", + render_core_tool_group(TOOL_TAXONOMY_DISCOVERY, core_tools), + render_core_tool_group(TOOL_TAXONOMY_GIT, core_tools), + render_core_tool_group(TOOL_TAXONOMY_VERIFICATION, core_tools) + ) +} + +fn render_core_tool_group(group: &[&str], core_tools: &[&str]) -> String { + group + .iter() + .copied() + .filter(|tool| core_tools.contains(tool)) + .map(|tool| format!("`{tool}`")) + .collect::>() + .join("/") +} + /// Authority recap block — appended at the end of the system prompt, /// just before the user's first message. Uses recency bias constructively: /// this is the last thing the model reads before generating, so it @@ -541,8 +566,11 @@ pub fn compose_prompt_with_approval_and_model( approval_mode: ApprovalMode, model_id: &str, ) -> String { - let parts: [&str; 4] = [ - &apply_model_template(BASE_PROMPT.trim(), model_id), + let tool_taxonomy = render_core_tool_taxonomy_block(); + let base_prompt = apply_model_template(BASE_PROMPT.trim(), model_id); + let parts: [&str; 5] = [ + tool_taxonomy.as_str(), + base_prompt.as_str(), personality.prompt().trim(), mode_prompt(mode).trim(), approval_prompt_for_mode(mode, approval_mode).trim(), @@ -993,6 +1021,36 @@ mod tests { ); } + #[test] + fn composed_prompt_starts_with_core_tool_taxonomy() { + let prompt = compose_prompt_with_approval_and_model( + AppMode::Agent, + Personality::Calm, + ApprovalMode::Suggest, + "deepseek-v4-pro", + ); + + assert!( + prompt.starts_with("## Core Tool Taxonomy\n\nUse `grep_files`/`file_search` for discovery. Use `git_status`/`git_diff` for git inspection. Use `run_tests` for verification."), + "composed prompt should start with the compact generated tool taxonomy" + ); + } + + #[test] + fn core_tool_taxonomy_only_references_default_active_tools() { + let core_tools = crate::core::engine::default_active_native_tool_names(); + for tool in TOOL_TAXONOMY_DISCOVERY + .iter() + .chain(TOOL_TAXONOMY_GIT) + .chain(TOOL_TAXONOMY_VERIFICATION) + { + assert!( + core_tools.contains(tool), + "tool taxonomy references {tool}, but it is not in the eager native-tool list" + ); + } + } + #[test] fn authority_recap_appears_in_full_prompt() { let tmp = tempdir().expect("tempdir"); @@ -1337,10 +1395,10 @@ mod tests { !text.contains("Reforço de Idioma"), "English locale must not get a pt-BR closer: {text:?}" ); - assert!( - !contains_cjk(&text), - "English system prompt should avoid native-script priming tokens: {text:?}" - ); + // Do not assert on arbitrary CJK in the full system prompt: project + // context may legitimately contain localized file names, README text, + // or user-authored instructions. The locale bookend markers above are + // the priming tokens this test is meant to guard. } #[test] From 539f6b5eaec7e37a0ade1d919ef851512e4d7b77 Mon Sep 17 00:00:00 2001 From: LING71671 <1739677116@qq.com> Date: Wed, 27 May 2026 23:49:40 +0800 Subject: [PATCH 2/2] fix(prompt): make tool taxonomy mode-aware --- crates/tui/src/prompts.rs | 84 +++++++++++++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 12 deletions(-) diff --git a/crates/tui/src/prompts.rs b/crates/tui/src/prompts.rs index 5e01ea927..671920eee 100644 --- a/crates/tui/src/prompts.rs +++ b/crates/tui/src/prompts.rs @@ -511,24 +511,45 @@ const TOOL_TAXONOMY_DISCOVERY: &[&str] = &["grep_files", "file_search"]; const TOOL_TAXONOMY_GIT: &[&str] = &["git_status", "git_diff"]; const TOOL_TAXONOMY_VERIFICATION: &[&str] = &["run_tests"]; -fn render_core_tool_taxonomy_block() -> String { +fn render_core_tool_taxonomy_block(mode: AppMode) -> String { + let core_tools = core_taxonomy_tools_for_mode(mode); + let mut sentences = Vec::new(); + + if let Some(discovery) = render_core_tool_group(TOOL_TAXONOMY_DISCOVERY, &core_tools) { + sentences.push(format!("Use {discovery} for discovery.")); + } + if let Some(git) = render_core_tool_group(TOOL_TAXONOMY_GIT, &core_tools) { + sentences.push(format!("Use {git} for git inspection.")); + } + if let Some(verification) = render_core_tool_group(TOOL_TAXONOMY_VERIFICATION, &core_tools) { + sentences.push(format!("Use {verification} for verification.")); + } + + debug_assert!( + !sentences.is_empty(), + "core tool taxonomy has no active tool groups" + ); + format!("## Core Tool Taxonomy\n\n{}", sentences.join(" ")) +} + +fn core_taxonomy_tools_for_mode(mode: AppMode) -> Vec<&'static str> { let core_tools = crate::core::engine::default_active_native_tool_names(); - format!( - "## Core Tool Taxonomy\n\nUse {} for discovery. Use {} for git inspection. Use {} for verification.", - render_core_tool_group(TOOL_TAXONOMY_DISCOVERY, core_tools), - render_core_tool_group(TOOL_TAXONOMY_GIT, core_tools), - render_core_tool_group(TOOL_TAXONOMY_VERIFICATION, core_tools) - ) + core_tools + .iter() + .copied() + .filter(|tool| mode != AppMode::Plan || *tool != "run_tests") + .collect() } -fn render_core_tool_group(group: &[&str], core_tools: &[&str]) -> String { - group +fn render_core_tool_group(group: &[&str], core_tools: &[&str]) -> Option { + let rendered = group .iter() .copied() .filter(|tool| core_tools.contains(tool)) .map(|tool| format!("`{tool}`")) .collect::>() - .join("/") + .join("/"); + (!rendered.is_empty()).then_some(rendered) } /// Authority recap block — appended at the end of the system prompt, @@ -566,7 +587,7 @@ pub fn compose_prompt_with_approval_and_model( approval_mode: ApprovalMode, model_id: &str, ) -> String { - let tool_taxonomy = render_core_tool_taxonomy_block(); + let tool_taxonomy = render_core_tool_taxonomy_block(mode); let base_prompt = apply_model_template(BASE_PROMPT.trim(), model_id); let parts: [&str; 5] = [ tool_taxonomy.as_str(), @@ -1029,13 +1050,41 @@ mod tests { ApprovalMode::Suggest, "deepseek-v4-pro", ); + let expected_taxonomy = render_core_tool_taxonomy_block(AppMode::Agent); assert!( - prompt.starts_with("## Core Tool Taxonomy\n\nUse `grep_files`/`file_search` for discovery. Use `git_status`/`git_diff` for git inspection. Use `run_tests` for verification."), + prompt.starts_with(&expected_taxonomy), "composed prompt should start with the compact generated tool taxonomy" ); } + #[test] + fn plan_prompt_taxonomy_omits_run_tests() { + let prompt = compose_prompt_with_approval_and_model( + AppMode::Plan, + Personality::Calm, + ApprovalMode::Never, + "deepseek-v4-pro", + ); + let expected_taxonomy = render_core_tool_taxonomy_block(AppMode::Plan); + + assert!( + prompt.starts_with(&expected_taxonomy), + "Plan prompt should start with its mode-specific tool taxonomy" + ); + assert!( + expected_taxonomy.contains("for discovery") + && expected_taxonomy.contains("for git inspection"), + "Plan taxonomy should keep read-only discovery and git guidance" + ); + assert!( + !expected_taxonomy.contains("run_tests") + && !expected_taxonomy.contains("for verification") + && !expected_taxonomy.contains("Use "), + "Plan taxonomy must not advertise unavailable verification tools: {expected_taxonomy:?}" + ); + } + #[test] fn core_tool_taxonomy_only_references_default_active_tools() { let core_tools = crate::core::engine::default_active_native_tool_names(); @@ -1395,6 +1444,17 @@ mod tests { !text.contains("Reforço de Idioma"), "English locale must not get a pt-BR closer: {text:?}" ); + assert!( + !contains_cjk(BASE_PROMPT), + "base prompt must not contain static CJK priming tokens" + ); + for mode in [AppMode::Agent, AppMode::Plan, AppMode::Yolo] { + let taxonomy = render_core_tool_taxonomy_block(mode); + assert!( + !contains_cjk(&taxonomy), + "tool taxonomy must not contain static CJK priming tokens: {taxonomy:?}" + ); + } // Do not assert on arbitrary CJK in the full system prompt: project // context may legitimately contain localized file names, README text, // or user-authored instructions. The locale bookend markers above are