@@ -1276,32 +1276,45 @@ pub async fn run_tool_call_loop(
12761276 . as_ref ( )
12771277 . and_then ( |usage| record_tool_loop_cost_usage ( provider_name, model, usage) ) ;
12781278
1279- let response_text = resp. text_or_empty ( ) . to_string ( ) ;
1279+ let response_text = if tool_specs. is_empty ( ) {
1280+ strip_think_tags ( resp. text_or_empty ( ) )
1281+ } else {
1282+ resp. text_or_empty ( ) . to_string ( )
1283+ } ;
12801284 // First try native structured tool calls (OpenAI-format).
12811285 // Fall back to text-based parsing (XML tags, markdown blocks,
12821286 // GLM format) only if the provider returned no native calls β
12831287 // this ensures we support both native and prompt-guided models.
1284- let mut calls: Vec < ParsedToolCall > = resp
1285- . tool_calls
1286- . iter ( )
1287- . map ( |call| ParsedToolCall {
1288- name : call. name . clone ( ) ,
1289- arguments : serde_json:: from_str :: < serde_json:: Value > ( & call. arguments )
1290- . unwrap_or_else ( |_| serde_json:: Value :: Object ( serde_json:: Map :: new ( ) ) ) ,
1291- tool_call_id : Some ( call. id . clone ( ) ) ,
1292- } )
1293- . collect ( ) ;
1288+ let mut calls: Vec < ParsedToolCall > = if tool_specs. is_empty ( ) {
1289+ Vec :: new ( )
1290+ } else {
1291+ resp. tool_calls
1292+ . iter ( )
1293+ . map ( |call| ParsedToolCall {
1294+ name : call. name . clone ( ) ,
1295+ arguments : serde_json:: from_str :: < serde_json:: Value > ( & call. arguments )
1296+ . unwrap_or_else ( |_| {
1297+ serde_json:: Value :: Object ( serde_json:: Map :: new ( ) )
1298+ } ) ,
1299+ tool_call_id : Some ( call. id . clone ( ) ) ,
1300+ } )
1301+ . collect ( )
1302+ } ;
12941303 let mut parsed_text = String :: new ( ) ;
12951304
1296- if calls. is_empty ( ) {
1305+ if calls. is_empty ( ) && !tool_specs . is_empty ( ) {
12971306 let ( fallback_text, fallback_calls) = parse_tool_calls ( & response_text) ;
12981307 if !fallback_text. is_empty ( ) {
12991308 parsed_text = fallback_text;
13001309 }
13011310 calls = fallback_calls;
13021311 }
13031312
1304- let parse_issue = detect_tool_call_parse_issue ( & response_text, & calls) ;
1313+ let parse_issue = if tool_specs. is_empty ( ) {
1314+ None
1315+ } else {
1316+ detect_tool_call_parse_issue ( & response_text, & calls)
1317+ } ;
13051318 if let Some ( ref issue) = parse_issue {
13061319 runtime_trace:: record_event (
13071320 "tool_call_parse_issue" ,
@@ -2060,6 +2073,29 @@ pub async fn run_tool_call_loop(
20602073/// Build the tool instruction block for the system prompt so the LLM knows
20612074/// how to invoke tools.
20622075pub fn build_tool_instructions ( tools_registry : & [ Box < dyn Tool > ] ) -> String {
2076+ build_tool_instructions_for_tools ( tools_registry. iter ( ) . map ( |tool| tool. as_ref ( ) ) )
2077+ }
2078+
2079+ /// Build tool instructions for the subset of registered tools that are
2080+ /// effective for the current prompt.
2081+ pub fn build_tool_instructions_for_names (
2082+ tools_registry : & [ Box < dyn Tool > ] ,
2083+ effective_tool_names : & HashSet < & str > ,
2084+ ) -> String {
2085+ build_tool_instructions_for_tools (
2086+ tools_registry
2087+ . iter ( )
2088+ . map ( |tool| tool. as_ref ( ) )
2089+ . filter ( |tool| effective_tool_names. contains ( tool. name ( ) ) ) ,
2090+ )
2091+ }
2092+
2093+ fn build_tool_instructions_for_tools < ' a > ( tools : impl IntoIterator < Item = & ' a dyn Tool > ) -> String {
2094+ let tools: Vec < & dyn Tool > = tools. into_iter ( ) . collect ( ) ;
2095+ if tools. is_empty ( ) {
2096+ return String :: new ( ) ;
2097+ }
2098+
20632099 let mut instructions = String :: new ( ) ;
20642100 instructions. push_str ( "\n ## Tool Use Protocol\n \n " ) ;
20652101 instructions. push_str ( "To use a tool, wrap a JSON object in <tool_call></tool_call> tags:\n \n " ) ;
@@ -2074,7 +2110,7 @@ pub fn build_tool_instructions(tools_registry: &[Box<dyn Tool>]) -> String {
20742110 . push_str ( "Continue reasoning with the results until you can give a final answer.\n \n " ) ;
20752111 instructions. push_str ( "### Available Tools\n \n " ) ;
20762112
2077- for tool in tools_registry {
2113+ for tool in tools {
20782114 let desc = tool. description ( ) ;
20792115 let _ = writeln ! (
20802116 instructions,
@@ -2088,6 +2124,15 @@ pub fn build_tool_instructions(tools_registry: &[Box<dyn Tool>]) -> String {
20882124 instructions
20892125}
20902126
2127+ fn retain_registered_tool_descriptions (
2128+ tool_descs : & mut Vec < ( & str , & str ) > ,
2129+ tools_registry : & [ Box < dyn Tool > ] ,
2130+ ) {
2131+ let registered_tool_names: HashSet < & str > =
2132+ tools_registry. iter ( ) . map ( |tool| tool. name ( ) ) . collect ( ) ;
2133+ tool_descs. retain ( |( name, _) | registered_tool_names. contains ( name) ) ;
2134+ }
2135+
20912136// ββ CLI Entrypoint βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
20922137// Wires up all subsystems (observer, runtime, security, memory, tools,
20932138// provider, hardware RAG, peripherals) and enters either single-shot or
@@ -2452,6 +2497,7 @@ pub async fn run(
24522497 "Query connected hardware for reported GPIO pins and LED pin. Use when: user asks what pins are available." ,
24532498 ) ) ;
24542499 }
2500+ retain_registered_tool_descriptions ( & mut tool_descs, & tools_registry) ;
24552501 let bootstrap_max_chars = if config. agent . compact_context {
24562502 Some ( 6000 )
24572503 } else {
@@ -3377,6 +3423,19 @@ pub async fn process_message(
33773423 tool_descs. retain ( |( name, _) | !excluded. iter ( ) . any ( |ex| ex == name) ) ;
33783424 }
33793425 }
3426+ let effective_tool_names: HashSet < & str > = tools_registry
3427+ . iter ( )
3428+ . map ( |tool| tool. name ( ) )
3429+ . filter ( |name| {
3430+ config. autonomy . level == AutonomyLevel :: Full
3431+ || !config
3432+ . autonomy
3433+ . non_cli_excluded_tools
3434+ . iter ( )
3435+ . any ( |excluded| excluded. as_str ( ) == * name)
3436+ } )
3437+ . collect ( ) ;
3438+ tool_descs. retain ( |( name, _) | effective_tool_names. contains ( name) ) ;
33803439
33813440 let bootstrap_max_chars = if config. agent . compact_context {
33823441 Some ( 6000 )
@@ -3398,7 +3457,10 @@ pub async fn process_message(
33983457 config. agent . max_system_prompt_chars ,
33993458 ) ;
34003459 if !native_tools {
3401- system_prompt. push_str ( & build_tool_instructions ( & tools_registry) ) ;
3460+ system_prompt. push_str ( & build_tool_instructions_for_names (
3461+ & tools_registry,
3462+ & effective_tool_names,
3463+ ) ) ;
34023464 }
34033465 if !deferred_section. is_empty ( ) {
34043466 system_prompt. push ( '\n' ) ;
@@ -6301,6 +6363,14 @@ mod tests {
63016363 assert ! ( instructions. contains( "file_write" ) ) ;
63026364 }
63036365
6366+ #[ test]
6367+ fn build_tool_instructions_empty_registry_returns_empty ( ) {
6368+ let tools: Vec < Box < dyn Tool > > = vec ! [ ] ;
6369+ let instructions = build_tool_instructions ( & tools) ;
6370+
6371+ assert ! ( instructions. is_empty( ) ) ;
6372+ }
6373+
63046374 #[ test]
63056375 fn tools_to_openai_format_produces_valid_schema ( ) {
63066376 use crate :: security:: SecurityPolicy ;
@@ -6942,6 +7012,50 @@ Let me check the result."#;
69427012 ) ;
69437013 }
69447014
7015+ #[ test]
7016+ fn non_native_system_prompt_with_no_tools_contains_zero_tool_protocol ( ) {
7017+ use crate :: agent:: system_prompt:: build_system_prompt_with_mode;
7018+
7019+ let tool_summaries: Vec < ( & str , & str ) > = vec ! [ ] ;
7020+
7021+ let system_prompt = build_system_prompt_with_mode (
7022+ std:: path:: Path :: new ( "/tmp" ) ,
7023+ "test-model" ,
7024+ & tool_summaries,
7025+ & [ ] ,
7026+ None ,
7027+ None ,
7028+ false ,
7029+ zeroclaw_config:: schema:: SkillsPromptInjectionMode :: Full ,
7030+ crate :: security:: AutonomyLevel :: default ( ) ,
7031+ ) ;
7032+
7033+ assert ! (
7034+ !system_prompt. contains( "## Tools" ) ,
7035+ "No-tools prompt must not include a Tools section"
7036+ ) ;
7037+ assert ! (
7038+ !system_prompt. contains( "## Tool Use Protocol" ) ,
7039+ "No-tools prompt must not include tool protocol"
7040+ ) ;
7041+ assert ! (
7042+ !system_prompt. contains( "<tool_call>" ) ,
7043+ "No-tools prompt must not mention XML tool calls"
7044+ ) ;
7045+ assert ! (
7046+ !system_prompt. contains( "<tool_result>" ) ,
7047+ "No-tools prompt must not mention XML tool results"
7048+ ) ;
7049+ assert ! (
7050+ !system_prompt. contains( "Use the tools" ) ,
7051+ "No-tools prompt must not instruct the model to use unavailable tools"
7052+ ) ;
7053+ assert ! (
7054+ system_prompt. contains( "No tools are available for this turn" ) ,
7055+ "No-tools prompt should explicitly describe the current capability boundary"
7056+ ) ;
7057+ }
7058+
69457059 // ββ Cross-Alias & GLM Shortened Body Tests ββββββββββββββββββββββββββ
69467060
69477061 #[ test]
0 commit comments