1use crate::config::constants::tools;
2use crate::config::types::{CapabilityLevel, ResolvedShellPromptProfile};
3use crate::core::agent::harness_kernel::SessionToolCatalogSnapshot;
4use crate::llm::provider::ToolDefinition;
5use crate::prompts::sections::{
6 SectionBoundaryMode, append_prompt_block, remove_prompt_section, trim_trailing_newlines,
7};
8use crate::tools::registry::tool_groups;
9
10const TOOL_EXEC_COMMAND: &str = tools::EXEC_COMMAND;
11const TOOL_WRITE_STDIN: &str = tools::WRITE_STDIN;
12const TOOL_CODE_SEARCH: &str = tools::CODE_SEARCH;
13const TOOL_READ_FILE: &str = tools::READ_FILE;
14const TOOL_LIST_FILES: &str = tools::LIST_FILES;
15const TOOL_GREP_FILE: &str = tools::GREP_FILE;
16const TOOL_APPLY_PATCH: &str = tools::APPLY_PATCH;
17const TOOL_REQUEST_USER_INPUT: &str = tools::REQUEST_USER_INPUT;
18const TOOL_TASK_TRACKER: &str = tools::TASK_TRACKER;
19const SOURCE_INSPECTION_GUIDANCE: &str =
20 "- Inspect relevant source, edit a bounded region, refresh after mismatch, then verify.";
21const PUBLIC_DECISION_GUIDANCE: &str = "- Use optional `record_decision` for consequential choices, rejected approaches, or recovery changes. Give concise public rationale; ordinary reads and commands need no record.";
22const MATRIX_GUIDANCE: &str = "- `matrix`: create persists explicit tasks, checks, resources, and timeouts; start freezes the spec. The coordinator delegates execution and verification to scheduler-owned workers; discovery is idle-only. Replay restores admission before discovery; resume reconciles owned cleanup. status is a projection; pause stops dispatch; user cancel is terminal. Internal failures require reconciliation, not cancellation. Workers retain effective permissions and sandbox gates. Read leases require read-only workers. Report identity is runtime-owned; final success needs durable declared checks for the current generation, never summaries or child completion. Source and declared input changes, including submodule state, invalidate checks. Automatic retry requires replay_safe, confirmed cleanup, and at most one interrupted/timeout retry; other failures need a coordinator decision.";
23const TOOL_START_PLANNING: &str = tools::START_PLANNING;
24
25const OPTIONAL_MARKDOWN_VALIDATION_GUIDANCE: &str = "- `verify: [skip Markdown lint if unavailable]`: report skipped; review diff/links without installing tools. Lint errors remain failures.";
26
27const CROSS_TURN_RESUME_HINT_CLAUSE: &str =
32 "; a turn-start `Exec session resume:` hint carries the live ids when a prior turn ended mid-run.";
33
34const START_PLANNING_GUIDANCE_LINE: &str = "- For demanding, ambiguous, or multi-phase tasks, call `start_planning`; it asks the user before entering the read-only Planning workflow unless the session runs in full-auto or skips confirmations. Skip it for straightforward changes.";
39
40const PLANNING_TASK_TRACKER_COMPACT_LINE: &str = "- `task_tracker`: list {\"action\":\"list\"}; updates use positive index/index_path or items, never conflicting fields; index 0 is invalid while planning. Keep blockers and verification open.";
45const PLANNING_TASK_TRACKER_INDEX_LINE: &str = "- `task_tracker`: list {\"action\":\"list\"}; update {\"action\":\"update\",\"index\":1,\"status\":\"in_progress\"}; bulk {\"action\":\"update\",\"items\":[\"[x] Done\",\"[ ] Remaining\"]}. Use positive flat indices or hierarchical index_path (index 0 is invalid while planning), or items for full checklist replacement. Omit unused fields; never combine index, index_path, and items.";
46
47const MINIMAL_CONTEXT_TOKEN_CEILING: usize = 32_000;
50
51const PARALLEL_TOOLS_LINE: &str = "- Run independent tools in parallel when inputs do not depend on each other.";
55
56const CODE_SEARCH_FILTER_LINE: &str = "- `code_search`: omit unused filters; no empty values (`path: \"\"`).";
59
60const REQUEST_USER_INPUT_LINE: &str =
63 "- Use `request_user_input` only for material blockers remaining after repository exploration.";
64
65const CAPABILITY_READ_ONLY_LINE: &str =
68 "- Capabilities: read-only. Analyze and search, but do not modify files or run shell commands.";
69
70const WRITE_STDIN_RECOVERY_CLAUSE: &str = "; if missing, recover prior output";
73
74const SHELL_PROFILE_POLICY_SUFFIX: &str = "- The shell profile controls prompt examples and expected command syntax only; command policy, sandboxing, and approvals remain separate runtime checks.\n- VT Code does not translate GNU-to-BSD, BSD-to-GNU, Unix-to-PowerShell, or PowerShell-to-Unix command flags.";
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub(crate) enum ToolGuidanceProfile {
81 Minimal,
82 Default,
83}
84
85impl ToolGuidanceProfile {
86 #[must_use]
87 pub(crate) fn resolve(
88 context_tokens: usize,
89 default_prompt_tokens: usize,
90 max_prompt_tokens: usize,
91 input_usd_per_token: Option<f64>,
92 max_budget_usd: Option<f64>,
93 ) -> Self {
94 let exceeds_cost = input_usd_per_token.zip(max_budget_usd).is_some_and(|(price, budget)| {
95 price.is_finite() && price >= 0.0 && budget.is_finite() && default_prompt_tokens as f64 * price > budget
96 });
97 if (context_tokens > 0 && context_tokens <= MINIMAL_CONTEXT_TOKEN_CEILING)
98 || (max_prompt_tokens > 0 && default_prompt_tokens > max_prompt_tokens)
99 || exceeds_cost
100 {
101 Self::Minimal
102 } else {
103 Self::Default
104 }
105 }
106}
107
108struct ToolPresence {
111 exec: bool,
112 write_stdin: bool,
113 code_search: bool,
114 read_file: bool,
115 grep_file: bool,
116 list_files: bool,
117 apply_patch: bool,
118 request_user_input: bool,
119 task_tracker: bool,
120 start_planning: bool,
121 matrix: bool,
122 record_decision: bool,
123}
124
125impl ToolPresence {
126 fn of(available_tools: &[String]) -> Self {
127 let mut presence = Self {
131 exec: false,
132 write_stdin: false,
133 code_search: false,
134 read_file: false,
135 grep_file: false,
136 list_files: false,
137 apply_patch: false,
138 request_user_input: false,
139 task_tracker: false,
140 start_planning: false,
141 matrix: false,
142 record_decision: false,
143 };
144 for tool in available_tools {
145 match tool.as_str() {
146 TOOL_EXEC_COMMAND => presence.exec = true,
147 TOOL_WRITE_STDIN => presence.write_stdin = true,
148 TOOL_CODE_SEARCH => presence.code_search = true,
149 TOOL_READ_FILE => presence.read_file = true,
150 TOOL_GREP_FILE => presence.grep_file = true,
151 TOOL_LIST_FILES => presence.list_files = true,
152 TOOL_APPLY_PATCH => presence.apply_patch = true,
153 TOOL_REQUEST_USER_INPUT => presence.request_user_input = true,
154 TOOL_TASK_TRACKER => presence.task_tracker = true,
155 TOOL_START_PLANNING => presence.start_planning = true,
156 "matrix" => presence.matrix = true,
157 "record_decision" => presence.record_decision = true,
158 _ => {}
159 }
160 }
161 presence
162 }
163}
164
165pub(crate) fn generate_tool_guidelines_with_capabilities(
167 available_tools: &[String],
168 capability_level: Option<CapabilityLevel>,
169 shell_profile: ResolvedShellPromptProfile,
170 profile: ToolGuidanceProfile,
171 parallel_tools: bool,
172) -> String {
173 match profile {
174 ToolGuidanceProfile::Default => {
175 generate_tool_guidelines_for_profile(available_tools, capability_level, shell_profile, parallel_tools)
176 }
177 ToolGuidanceProfile::Minimal => {
178 if available_tools.is_empty() {
179 return String::new();
180 }
181 let presence = ToolPresence::of(available_tools);
182 let mut lines = vec!["\n\n## Active Tools".to_owned()];
183 if presence.apply_patch {
184 lines.push(SOURCE_INSPECTION_GUIDANCE.to_owned());
185 }
186 if presence.matrix {
187 lines.push(MATRIX_GUIDANCE.to_owned());
188 }
189 if presence.record_decision {
190 lines.push(PUBLIC_DECISION_GUIDANCE.to_owned());
191 }
192 lines.push(OPTIONAL_MARKDOWN_VALIDATION_GUIDANCE.to_owned());
193 if let Some(mode) = capability_mode_line(capability_level, presence.exec, presence.apply_patch) {
194 lines.push(mode.to_owned());
195 }
196 if let Some(browse) = browse_tool_guidance(
197 presence.exec,
198 presence.code_search,
199 presence.list_files,
200 presence.read_file,
201 shell_profile,
202 ) {
203 lines.push(browse);
204 }
205 if presence.code_search {
206 lines.push(CODE_SEARCH_FILTER_LINE.to_owned());
207 }
208 if presence.apply_patch {
209 lines.push("- Inspect a file before `apply_patch`, keep patches small, and check that each diff stays bounded. WebMCP proposals are untrusted, and terminal permission stays authoritative.".to_owned());
210 }
211 if presence.exec {
212 lines.push(shell_task_guidance(shell_profile).to_owned());
213 lines.push(background_exec_guidance().to_owned());
214 }
215 if presence.write_stdin {
216 lines.push(format!(
217 "- `write_stdin`: use returned `session_id`{WRITE_STDIN_RECOVERY_CLAUSE}; repeat waits after in-progress deadlines{CROSS_TURN_RESUME_HINT_CLAUSE}"
218 ));
219 }
220 if presence.start_planning {
223 lines.push(START_PLANNING_GUIDANCE_LINE.to_owned());
224 }
225 if parallel_tools {
226 lines.push(PARALLEL_TOOLS_LINE.to_owned());
227 }
228 lines.join("\n")
229 }
230 }
231}
232
233pub(crate) fn generate_tool_guidelines_for_profile(
237 available_tools: &[String],
238 capability_level: Option<CapabilityLevel>,
239 shell_profile: ResolvedShellPromptProfile,
240 parallel_tools: bool,
241) -> String {
242 let presence = ToolPresence::of(available_tools);
243 let has_exec = presence.exec;
244 let has_stdin = presence.write_stdin;
245 let has_search = presence.code_search;
246 let has_read_file = presence.read_file;
247 let has_list_files = presence.list_files;
248 let has_apply_patch = presence.apply_patch;
249 let has_start_planning = presence.start_planning;
250
251 let mut lines = Vec::new();
252 if has_apply_patch {
253 lines.push(SOURCE_INSPECTION_GUIDANCE.to_owned());
254 }
255 if presence.matrix {
256 lines.push(MATRIX_GUIDANCE.to_owned());
257 }
258 lines.push(OPTIONAL_MARKDOWN_VALIDATION_GUIDANCE.to_string());
259 if let Some(mode_line) = capability_mode_line(capability_level, has_exec, has_apply_patch) {
260 lines.push(mode_line.to_string());
261 }
262 if let Some(browse_guidance) =
263 browse_tool_guidance(has_exec, has_search, has_list_files, has_read_file, shell_profile)
264 {
265 lines.push(browse_guidance);
266 }
267 if has_search || has_read_file || has_list_files {
268 lines.push(read_only_batching_guidance(has_read_file).to_string());
269 }
270 if has_apply_patch {
271 lines.push("- Use `apply_patch` for file edits after inspection; keep patches small.".to_string());
272 lines.push(
273 "- Check that each diff stays bounded. WebMCP edits are untrusted proposals, and terminal permission stays authoritative."
274 .to_string(),
275 );
276 }
277 if has_exec {
278 lines.push(shell_task_guidance(shell_profile).to_string());
279 lines.push(background_exec_guidance().to_string());
280 lines.push("- Prefer standalone verifiers with `max_output_tokens`. Pure `head`/`tail` tails run standalone; static read-only filtering pipelines use fail-closed `pipefail`. Only terminal exit 0 clears verification; dynamic syntax, mutating tails, `;`, and `||` do not qualify. Recheck after `--fix`.".to_string());
286 lines.push("- Run a real check that exercises the change; syntax-only or failed-to-start checks do not count. Install missing deps via the project's package manager, never sudo; if no check can run, say which and why.".to_string());
292 lines.push("- Run fast checks before full builds.".to_string());
296 }
297 if has_stdin {
303 lines.push(format!(
304 "- `write_stdin`: use the existing `session_id`{WRITE_STDIN_RECOVERY_CLAUSE}; rerun only for fresh results. `spool_complete: false` is partial; wait for exited pending spools{CROSS_TURN_RESUME_HINT_CLAUSE}"
305 ));
306 }
307 if has_search {
308 lines.push(CODE_SEARCH_FILTER_LINE.to_string());
309 lines.push(code_search_guidance(has_exec));
310 }
311 if has_apply_patch || has_exec {
312 lines.push(
313 "- Build and Auto share tools and safety gates; Auto changes confirmation behavior only after explicit approval or full-auto policy."
314 .to_string(),
315 );
316 }
317 if (has_search || has_exec) && parallel_tools {
318 lines.push(PARALLEL_TOOLS_LINE.to_string());
319 }
320 if has_start_planning {
321 lines.push(START_PLANNING_GUIDANCE_LINE.to_string());
322 }
323
324 if lines.is_empty() {
325 return String::new();
326 }
327
328 if presence.record_decision {
329 lines.push(PUBLIC_DECISION_GUIDANCE.to_owned());
330 }
331 format!("\n\n## Active Tools\n{}", lines.join("\n"))
332}
333
334pub fn append_runtime_tool_prompt_sections_for_profile(
335 prompt: &mut String,
336 tool_snapshot: &SessionToolCatalogSnapshot,
337 include_catalog_metadata: bool,
338 shell_profile: ResolvedShellPromptProfile,
339) {
340 let names = snapshot_tool_names(tool_snapshot);
341 append_runtime_tool_sections_with_names(prompt, tool_snapshot, names, include_catalog_metadata, shell_profile);
342}
343
344pub fn append_runtime_tool_prompt_sections_for_model(
346 prompt: &mut String,
347 tool_snapshot: &SessionToolCatalogSnapshot,
348 include_catalog_metadata: bool,
349 shell_profile: ResolvedShellPromptProfile,
350 provider: &dyn crate::llm::provider::LLMProvider,
351 model: &str,
352 config: Option<&crate::config::VTCodeConfig>,
353) {
354 let names = snapshot_tool_names(tool_snapshot);
355 let default_guidance =
356 append_runtime_tool_sections_with_names(prompt, tool_snapshot, names, include_catalog_metadata, shell_profile);
357 let pricing = crate::config::models::model_catalog_entry(provider.name(), model).map(|entry| entry.pricing);
358 let profile = ToolGuidanceProfile::resolve(
359 crate::compaction::effective_context_budget(config, provider, model),
360 vtcode_commons::estimate_tokens(prompt),
361 config.map_or(0, |cfg| cfg.agent.max_system_prompt_tokens as usize),
362 pricing.and_then(|price| price.input),
363 config.and_then(|cfg| cfg.agent.harness.max_budget_usd),
364 );
365 let parallel_tools = provider.supports_parallel_tool_config(model);
366 if tool_snapshot.planning_active && profile == ToolGuidanceProfile::Default {
369 return;
370 }
371 remove_prompt_section(prompt, "## Active Tools", SectionBoundaryMode::BracketOrMarkdown);
372 let presence = ToolPresence::of(names);
373 let capability_level = Some(infer_capability_level_from_presence(&presence));
374 let mut guidance = match profile {
375 ToolGuidanceProfile::Default => {
376 let mut guidance = default_guidance;
377 if let (Some(original), Some(effective)) = (
378 capability_mode_line(None, presence.exec, presence.apply_patch),
379 capability_mode_line(capability_level, presence.exec, presence.apply_patch),
380 ) && original != effective
381 && let Some(start) = guidance.find(original)
382 {
383 guidance.replace_range(start..start + original.len(), effective);
384 }
385 if !parallel_tools && let Some(start) = guidance.find(PARALLEL_TOOLS_LINE) {
386 guidance.replace_range(start.saturating_sub(1)..start + PARALLEL_TOOLS_LINE.len(), "");
387 }
388 guidance
389 }
390 ToolGuidanceProfile::Minimal => {
391 generate_tool_guidelines_with_capabilities(names, capability_level, shell_profile, profile, parallel_tools)
392 }
393 };
394 if tool_snapshot.planning_active {
395 append_minimal_planning_addendum(&mut guidance, &presence);
396 }
397 append_prompt_block(prompt, guidance.trim_start_matches('\n'));
398}
399
400fn append_runtime_tool_sections_with_names(
404 prompt: &mut String,
405 tool_snapshot: &SessionToolCatalogSnapshot,
406 names: &[String],
407 include_catalog_metadata: bool,
408 shell_profile: ResolvedShellPromptProfile,
409) -> String {
410 remove_prompt_section(prompt, "## Active Tools", SectionBoundaryMode::BracketOrMarkdown);
411 remove_prompt_section(prompt, "[Runtime Tool Catalog]", SectionBoundaryMode::BracketOrMarkdown);
412 trim_trailing_newlines(prompt);
413
414 let guidelines = generate_runtime_tool_guidelines_for_profile(names, tool_snapshot.planning_active, shell_profile);
415 if !guidelines.is_empty() {
416 append_prompt_block(prompt, guidelines.trim_start_matches('\n'));
417 }
418
419 if include_catalog_metadata && tool_snapshot.snapshot.is_some() {
420 let active_tools = if tool_snapshot.active_tool_names.is_empty() {
421 "none".to_string()
422 } else {
423 tool_snapshot.active_tool_names.join(", ")
424 };
425 let catalog_metadata = format!(
426 "[Runtime Tool Catalog]\n- version: {}\n- epoch: {}\n- catalog_tools: {}\n- available_tools: {}\n- currently_available_tools: {}\n- request_user_input_enabled: {}",
427 tool_snapshot.version,
428 tool_snapshot.epoch,
429 tool_snapshot.catalog_tools(),
430 tool_snapshot.available_tools(),
431 active_tools,
432 tool_snapshot.request_user_input_enabled,
433 );
434 append_prompt_block(prompt, &catalog_metadata);
435 }
436 guidelines
437}
438
439fn append_minimal_planning_addendum(guidance: &mut String, presence: &ToolPresence) {
442 guidance.push_str("\n- Planning is read-only.");
443 let mut read_tools = Vec::with_capacity(4);
444 if presence.read_file {
445 read_tools.push(TOOL_READ_FILE);
446 }
447 if presence.grep_file {
448 read_tools.push(TOOL_GREP_FILE);
449 }
450 if presence.code_search {
451 read_tools.push(TOOL_CODE_SEARCH);
452 }
453 if presence.list_files {
454 read_tools.push(TOOL_LIST_FILES);
455 }
456 guidance.push_str("\n- Keep inspections small: ");
457 if !read_tools.is_empty() {
458 guidance.push_str(&format!("prefer `{}` over `exec_command` shell reads; ", read_tools.join("`/`")));
459 }
460 guidance.push_str("keep `max_output_tokens` small, avoid batching multiple large inspections in parallel; start git history with `git log --oneline` before targeted `git show --stat`.");
461 if presence.task_tracker {
462 guidance.push('\n');
463 guidance.push_str(PLANNING_TASK_TRACKER_COMPACT_LINE);
464 }
465 if presence.request_user_input {
466 guidance.push('\n');
467 guidance.push_str(REQUEST_USER_INPUT_LINE);
468 }
469}
470
471pub fn append_deferred_tools_prompt_section(prompt: &mut String, tools: &[ToolDefinition]) {
479 remove_prompt_section(prompt, "[Deferred Tools]", SectionBoundaryMode::BracketOrMarkdown);
480
481 let mut groups: Vec<_> = tool_groups(tools)
482 .into_iter()
483 .filter(|group| group.deferred_count > 0)
484 .collect();
485 let overflow = groups.len().saturating_sub(DEFERRED_TOOLS_MAX_GROUPS);
486 groups.truncate(DEFERRED_TOOLS_MAX_GROUPS);
487
488 let mut lines: Vec<String> = groups
489 .into_iter()
490 .map(|group| {
491 let description = truncate_deferred_description(group.description.as_deref().unwrap_or_default());
492 format!("- {} ({} tools): {}", group.name, group.deferred_count, description)
493 })
494 .collect();
495 if overflow > 0 {
496 lines.push(format!("(+{overflow} more deferred groups available — use `search_tools` to find them)"));
497 }
498
499 let unnamespaced_deferred = tools
500 .iter()
501 .filter(|tool| tool.namespace.is_none() && tool.defer_loading == Some(true))
502 .count();
503 if unnamespaced_deferred > 0 {
504 lines.push(format!("- {unnamespaced_deferred} additional deferred tools"));
505 }
506
507 if lines.is_empty() {
508 return;
509 }
510
511 let section = format!(
512 "[Deferred Tools]\n{}\nUse `search_tools` to find a deferred capability. Selected definitions become available in the next request segment.",
513 lines.join("\n")
514 );
515 append_prompt_block(prompt, §ion);
516}
517
518const DEFERRED_TOOLS_MAX_GROUPS: usize = 8;
521const DEFERRED_TOOLS_MAX_DESC_CHARS: usize = 120;
524
525fn truncate_deferred_description(description: &str) -> String {
526 let mut chars = description.chars();
529 let truncated: String = chars.by_ref().take(DEFERRED_TOOLS_MAX_DESC_CHARS).collect();
530 if chars.next().is_none() {
531 return description.to_string();
532 }
533 format!("{}...", truncated.trim_end())
534}
535
536fn generate_runtime_tool_guidelines_for_profile(
537 available_tools: &[String],
538 planning_active: bool,
539 shell_profile: ResolvedShellPromptProfile,
540) -> String {
541 if !planning_active {
542 return generate_tool_guidelines_for_profile(available_tools, None, shell_profile, true);
546 }
547
548 let presence = ToolPresence::of(available_tools);
549 let has_exec = presence.exec;
550 let has_search = presence.code_search;
551 let has_read_file = presence.read_file;
552 let has_list_files = presence.list_files;
553 let has_request_user_input = presence.request_user_input;
554 let has_task_tracker = presence.task_tracker;
555
556 let mut lines = vec!["- Planning workflow active: stay within the read-safe tool list.".to_string()];
557 lines.push(OPTIONAL_MARKDOWN_VALIDATION_GUIDANCE.to_string());
558 if let Some(browse_guidance) =
559 browse_tool_guidance(has_exec, has_search, has_list_files, has_read_file, shell_profile)
560 {
561 lines.push(browse_guidance);
562 }
563 if has_exec {
564 lines.push("- In Planning workflow, use `exec_command` only for read-only verification.".to_string());
565 }
566 if has_search {
567 lines.push(CODE_SEARCH_FILTER_LINE.to_string());
568 }
569 if has_task_tracker {
570 lines.push("- Keep `task_tracker` updated as you refine the plan. Indexed updates return totals and the changed item; use action=list for the full checklist.".to_string());
571 lines.push("- Keep blockers and verification open in `task_tracker` until resolved.".to_string());
572 lines.push(PLANNING_TASK_TRACKER_INDEX_LINE.to_string());
573 }
574 if has_request_user_input {
575 lines.push(REQUEST_USER_INPUT_LINE.to_string());
576 }
577 if has_search || has_exec {
578 lines.push("- If calls repeat without progress, tighten the plan instead of retrying identically.".to_string());
579 }
580 if presence.record_decision {
581 lines.push(PUBLIC_DECISION_GUIDANCE.to_owned());
582 }
583
584 format!("\n\n## Active Tools\n{}", lines.join("\n"))
585}
586
587fn snapshot_tool_names(tool_snapshot: &SessionToolCatalogSnapshot) -> &[String] {
588 &tool_snapshot.active_tool_names
589}
590
591fn browse_tool_guidance(
592 has_exec: bool,
593 has_search: bool,
594 has_list_files: bool,
595 has_read_file: bool,
596 shell_profile: ResolvedShellPromptProfile,
597) -> Option<String> {
598 if has_exec {
599 return Some(shell_browse_guidance(shell_profile, has_search));
600 }
601
602 if !(has_search || has_list_files || has_read_file) {
603 return None;
604 }
605
606 Some("- Use available read-only repository tools for browsing; do not modify files.".to_string())
607}
608
609pub(crate) fn render_shell_profile_guidance(shell_profile: ResolvedShellPromptProfile) -> String {
610 let profile_lines = match shell_profile {
611 ResolvedShellPromptProfile::UnixLike => {
612 "- Active shell profile: `unix_like`. Use Unix-like command syntax in `exec_command.cmd`, for example `ls`, `rg`, `find`, `cat`, `sed`, and `awk`.\n- On macOS, write BSD-compatible flags for BSD tools. VT Code does not rewrite GNU flags for macOS BSD tools."
613 }
614 ResolvedShellPromptProfile::PowerShell => {
615 "- Active shell profile: `powershell`. Use native PowerShell syntax in `exec_command.cmd`, for example `Get-ChildItem`, `Select-String`, `Get-Content`, and `Where-Object`.\n- On native Windows, use WSL when you need Unix-like workflows or Unix command examples."
616 }
617 };
618 format!("## Shell Profile\n{profile_lines}\n{SHELL_PROFILE_POLICY_SUFFIX}")
619}
620
621fn shell_browse_guidance(shell_profile: ResolvedShellPromptProfile, has_search: bool) -> String {
622 const SEARCH_PREFERENCE_UNIX: &str = " Prefer `code_search` over `rg`/`grep` for code.";
627 const SEARCH_PREFERENCE_POWERSHELL: &str = " Prefer `code_search` over `Select-String` for code.";
628 match shell_profile {
629 ResolvedShellPromptProfile::UnixLike => {
630 let mut line = String::from("- Use `exec_command.cmd` with `ls`, ");
631 if !has_search {
632 line.push_str("`rg`, ");
633 }
634 line.push_str("`find`, `cat`, `sed`, and `awk` for repository browsing.");
635 if has_search {
636 line.push_str(SEARCH_PREFERENCE_UNIX);
637 }
638 line
639 }
640 ResolvedShellPromptProfile::PowerShell => {
641 let mut line = "- Use `exec_command.cmd` with native PowerShell commands such as `Get-ChildItem`, `Select-String`, `Get-Content`, and `Where-Object` for repository browsing.".to_string();
642 if has_search {
643 line.push_str(SEARCH_PREFERENCE_POWERSHELL);
644 }
645 line
646 }
647 }
648}
649
650fn shell_task_guidance(shell_profile: ResolvedShellPromptProfile) -> &'static str {
651 match shell_profile {
652 ResolvedShellPromptProfile::UnixLike => {
653 "- Use `exec_command.cmd` for build tools, test tools, `git diff -- <path>`, and shell-only tasks. In one-shot `exec_command` calls, do not use `!!`, `!$`, `!ssh`, or `fc`; write full command arguments explicitly from conversation or tool results. Interactive shells: review-safe history expansion (Bash `histverify`, zsh `HIST_VERIFY`)."
654 }
655 ResolvedShellPromptProfile::PowerShell => {
656 "- Use `exec_command.cmd` for build tools, test tools, `git diff -- <path>`, and shell-only tasks using native PowerShell syntax."
657 }
658 }
659}
660
661fn background_exec_guidance() -> &'static str {
662 "- For long-lived commands, set `background: true` on `exec_command`; it returns a bounded preview plus a stable `session_id` and wait arguments. At most three live background processes are retained per runtime, with no automatic eviction; `write_stdin` drives the session lifecycle."
663}
664
665fn read_only_batching_guidance(has_read_file: bool) -> &'static str {
666 if has_read_file {
667 "- Batch independent read-only calls; use bounded `read_file` ranges, order dependencies, serialize mutations; narrow the range on `line_truncated`; prefer wider windows and reuse evidence over overlapping slices."
668 } else {
669 "- Batch independent read-only calls; order dependent reads, and serialize mutations."
670 }
671}
672
673fn code_search_guidance(has_exec: bool) -> String {
674 const BASE: &str = "- Advanced `code_search` takes `query`; filters `path`, `file_types`, `result_types`, `max_results`; results: definitions, exact syntactic usages. Queries use literal smart-case and `|`-separated literals; truncated: narrow; unavailable: do not retry identical, drop `file_types`/broaden `path`. Example: `{\"query\":\"TurnLoop\",\"path\":\"src\",\"result_types\":[\"definition\"]}`. Do not JSON-encode arrays or integers as strings. Prefer `code_search` over `rg` on `.vtcode/context/tool_outputs/`.";
675 if has_exec {
676 format!("{BASE} Use `exec_command` or a skill for syntax patterns.")
677 } else {
678 BASE.to_string()
679 }
680}
681
682fn capability_mode_line(
683 capability_level: Option<CapabilityLevel>,
684 has_exec: bool,
685 has_file: bool,
686) -> Option<&'static str> {
687 match capability_level {
688 Some(CapabilityLevel::Basic) => {
689 Some("- Capabilities: limited. Ask the user to enable more capabilities if file work is required.")
690 }
691 Some(CapabilityLevel::FileReading | CapabilityLevel::FileListing) => Some(CAPABILITY_READ_ONLY_LINE),
692 _ if !has_exec && !has_file => Some(CAPABILITY_READ_ONLY_LINE),
693 _ => None,
694 }
695}
696
697pub(crate) fn infer_capability_level(available_tools: &[String]) -> CapabilityLevel {
699 infer_capability_level_from_presence(&ToolPresence::of(available_tools))
700}
701
702fn infer_capability_level_from_presence(presence: &ToolPresence) -> CapabilityLevel {
703 if presence.code_search {
704 CapabilityLevel::CodeSearch
705 } else if presence.apply_patch {
706 CapabilityLevel::Editing
707 } else if presence.exec {
708 CapabilityLevel::Bash
709 } else if presence.list_files {
710 CapabilityLevel::FileListing
711 } else if presence.read_file {
712 CapabilityLevel::FileReading
713 } else {
714 CapabilityLevel::Basic
715 }
716}
717
718#[cfg(test)]
719mod tests {
720 use super::*;
721 use crate::config::types::ShellPromptProfile;
722
723 fn guidelines_for(tools: &[String], capability_level: Option<CapabilityLevel>) -> String {
726 generate_tool_guidelines_for_profile(
727 tools,
728 capability_level,
729 ShellPromptProfile::Auto.resolve_for_current_platform(),
730 true,
731 )
732 }
733
734 fn rebuilt_model_tool_prompt(
737 mut prompt: String,
738 snapshot: &SessionToolCatalogSnapshot,
739 metadata: bool,
740 shell: ResolvedShellPromptProfile,
741 provider: &dyn crate::llm::provider::LLMProvider,
742 model: &str,
743 config: &crate::config::VTCodeConfig,
744 ) -> String {
745 append_runtime_tool_prompt_sections_for_profile(&mut prompt, snapshot, metadata, shell);
746 let names = snapshot_tool_names(snapshot);
747 let pricing = crate::config::models::model_catalog_entry(provider.name(), model).map(|entry| entry.pricing);
748 let profile = ToolGuidanceProfile::resolve(
749 crate::compaction::effective_context_budget(Some(config), provider, model),
750 vtcode_commons::estimate_tokens(&prompt),
751 config.agent.max_system_prompt_tokens as usize,
752 pricing.and_then(|price| price.input),
753 config.agent.harness.max_budget_usd,
754 );
755 if snapshot.planning_active && profile == ToolGuidanceProfile::Default {
756 return prompt;
757 }
758 remove_prompt_section(&mut prompt, "## Active Tools", SectionBoundaryMode::BracketOrMarkdown);
759 let presence = ToolPresence::of(names);
760 let mut guidance = generate_tool_guidelines_with_capabilities(
761 names,
762 Some(infer_capability_level_from_presence(&presence)),
763 shell,
764 profile,
765 provider.supports_parallel_tool_config(model),
766 );
767 if snapshot.planning_active {
768 append_minimal_planning_addendum(&mut guidance, &presence);
769 }
770 append_prompt_block(&mut prompt, guidance.trim_start_matches('\n'));
771 prompt
772 }
773
774 #[test]
775 fn model_tool_prompt_reuse_matches_rebuild_across_density_and_capabilities() {
776 use crate::config::constants::models;
777 use crate::llm::provider::LLMProvider;
778 use crate::llm::providers::{AnthropicProvider, HuggingFaceProvider};
779
780 struct ProviderCase {
781 provider: Box<dyn LLMProvider>,
782 model: &'static str,
783 parallel: bool,
784 }
785 let providers = [
786 ProviderCase {
787 provider: Box::new(AnthropicProvider::new("offline-fixture".into())),
788 model: models::anthropic::DEFAULT_MODEL,
789 parallel: true,
790 },
791 ProviderCase {
792 provider: Box::new(HuggingFaceProvider::new("offline-fixture".into())),
793 model: "offline-fixture",
794 parallel: false,
795 },
796 ];
797 let initial = "Base prompt\n\n## Active Tools\nold guidance\n\n# PLANNING WORKFLOW (READ-ONLY)\nKeep the canonical contract.\n\n[Runtime Tool Catalog]\nold metadata\n\n## Tail\nKeep this section.\n\n";
798 for case in providers {
799 assert_eq!(case.provider.supports_parallel_tool_config(case.model), case.parallel);
800 for names in [
801 None,
802 Some(&[][..]),
803 Some(&[TOOL_TASK_TRACKER, TOOL_REQUEST_USER_INPUT][..]),
804 Some(&[TOOL_READ_FILE][..]),
805 Some(&[TOOL_WRITE_STDIN][..]),
806 Some(&[TOOL_EXEC_COMMAND, TOOL_APPLY_PATCH, TOOL_CODE_SEARCH, "record_decision"][..]),
807 ] {
808 for planning in [false, true] {
809 let snapshot = SessionToolCatalogSnapshot::new(
810 7,
811 9,
812 planning,
813 true,
814 names.map(|names| {
815 std::sync::Arc::new(
816 names
817 .iter()
818 .map(|name| {
819 ToolDefinition::function(
820 (*name).to_string(),
821 "fixture".to_string(),
822 serde_json::json!({"type": "object"}),
823 )
824 })
825 .collect(),
826 )
827 }),
828 false,
829 );
830 for shell in [
831 ResolvedShellPromptProfile::UnixLike,
832 ResolvedShellPromptProfile::PowerShell,
833 ] {
834 for metadata in [false, true] {
835 let mut measured = initial.to_string();
836 append_runtime_tool_prompt_sections_for_profile(&mut measured, &snapshot, metadata, shell);
837 let measured_tokens = vtcode_commons::estimate_tokens(&measured) as u64;
838 for budget in [1, measured_tokens - 1, measured_tokens, 100_000] {
839 let mut config = crate::config::VTCodeConfig::default();
840 config.context.max_context_tokens = 128_000;
841 config.agent.max_system_prompt_tokens = budget;
842 let expected = rebuilt_model_tool_prompt(
843 initial.to_string(),
844 &snapshot,
845 metadata,
846 shell,
847 case.provider.as_ref(),
848 case.model,
849 &config,
850 );
851 let mut actual = initial.to_string();
852 append_runtime_tool_prompt_sections_for_model(
853 &mut actual,
854 &snapshot,
855 metadata,
856 shell,
857 case.provider.as_ref(),
858 case.model,
859 Some(&config),
860 );
861
862 assert_eq!(
863 actual,
864 expected,
865 "provider={}, names={names:?}, planning={planning}, shell={shell:?}, metadata={metadata}, budget={budget}",
866 case.provider.name()
867 );
868 assert_eq!(
869 actual.matches("## Active Tools").count(),
870 usize::from(
871 budget >= measured_tokens || names.is_some_and(|names| !names.is_empty())
872 )
873 );
874 assert!(
875 actual.contains("# PLANNING WORKFLOW (READ-ONLY)\nKeep the canonical contract.")
876 );
877 assert!(actual.contains("## Tail\nKeep this section."));
878 }
879 }
880 }
881 }
882 }
883 }
884 }
885
886 #[test]
887 fn matrix_guidance_has_presence_and_budget_in_both_densities() {
888 for profile in [ToolGuidanceProfile::Default, ToolGuidanceProfile::Minimal] {
889 let enabled = generate_tool_guidelines_with_capabilities(
890 &["matrix".to_string(), "request_user_input".to_string()],
891 None,
892 ResolvedShellPromptProfile::UnixLike,
893 profile,
894 false,
895 );
896 let disabled = generate_tool_guidelines_with_capabilities(
897 &["request_user_input".to_string()],
898 None,
899 ResolvedShellPromptProfile::UnixLike,
900 profile,
901 false,
902 );
903 assert_eq!(enabled.matches(MATRIX_GUIDANCE).count(), 1);
904 assert!(enabled.contains("declared input changes"));
905 assert!(enabled.contains("effective permissions and sandbox gates"));
906 assert!(enabled.contains("Replay restores admission before discovery"));
907 assert!(enabled.contains("user cancel is terminal"));
908 assert!(!disabled.contains("scheduler-owned"));
909 assert!(vtcode_commons::estimate_tokens(&enabled) <= 220, "{profile:?}: {enabled}");
910 }
911 }
912
913 #[test]
917 fn universal_rules_have_one_home_across_composed_prompt_sections() {
918 use crate::config::types::SystemPromptMode;
919 use crate::prompts::harness_limits::upsert_harness_limits_section;
920 use crate::prompts::static_prompts::static_profile_prompt;
921 use crate::prompts::system::PLANNING_WORKFLOW_READ_ONLY_NOTICE_LINE;
922
923 let shell = ResolvedShellPromptProfile::UnixLike;
924 let execution_tools = [
925 TOOL_EXEC_COMMAND,
926 TOOL_WRITE_STDIN,
927 TOOL_APPLY_PATCH,
928 TOOL_CODE_SEARCH,
929 TOOL_TASK_TRACKER,
930 TOOL_START_PLANNING,
931 TOOL_REQUEST_USER_INPUT,
932 ]
933 .map(str::to_owned)
934 .to_vec();
935 let planning_tools = [
936 TOOL_EXEC_COMMAND,
937 TOOL_WRITE_STDIN,
938 TOOL_CODE_SEARCH,
939 TOOL_GREP_FILE,
940 TOOL_TASK_TRACKER,
941 TOOL_REQUEST_USER_INPUT,
942 ]
943 .map(str::to_owned)
944 .to_vec();
945 let mut minimal_planning = generate_tool_guidelines_with_capabilities(
946 &planning_tools,
947 None,
948 shell,
949 ToolGuidanceProfile::Minimal,
950 false,
951 );
952 append_minimal_planning_addendum(&mut minimal_planning, &ToolPresence::of(&planning_tools));
953 let variants = [
954 ("default", generate_tool_guidelines_for_profile(&execution_tools, None, shell, true), false),
955 (
956 "minimal",
957 generate_tool_guidelines_with_capabilities(
958 &execution_tools,
959 None,
960 shell,
961 ToolGuidanceProfile::Minimal,
962 true,
963 ),
964 false,
965 ),
966 ("default planning", generate_runtime_tool_guidelines_for_profile(&planning_tools, true, shell), true),
967 ("minimal planning", minimal_planning, true),
968 ];
969 let markers = [
972 "never claim a check passed",
973 "Diagnose failures; change approach",
974 "Use returned `next_wait_args`",
975 "Treat empty searches as evidence",
976 "Check optional tools once",
977 "small non-overlapping ranges",
978 "accumulated output never exhausts tool access",
979 "additional_permissions",
980 "bypass safeguards",
981 "Delegate only sizeable",
982 "Across compaction",
983 ];
984
985 for mode in [
986 SystemPromptMode::Default,
987 SystemPromptMode::Minimal,
988 SystemPromptMode::Lightweight,
989 SystemPromptMode::Specialized,
990 ] {
991 for (variant, guidance, planning) in &variants {
992 let mut prompt = static_profile_prompt(mode).to_owned();
993 if *planning {
994 prompt.push_str("\n\n");
995 prompt.push_str(PLANNING_WORKFLOW_READ_ONLY_NOTICE_LINE);
996 }
997 prompt.push_str(guidance);
998 upsert_harness_limits_section(&mut prompt, 32, 600, 2);
999 for marker in markers {
1000 assert_eq!(
1001 prompt.matches(marker).count(),
1002 1,
1003 "{mode:?} with {variant} tool guidance should state {marker:?} exactly once"
1004 );
1005 }
1006 let start_planning_mentions = prompt.matches("start_planning").count();
1007 assert_eq!(start_planning_mentions, usize::from(!*planning), "{mode:?} with {variant} tool guidance");
1008 }
1009 }
1010 }
1011
1012 #[test]
1013 fn documentation_profile_respects_context_tokens_and_known_cost() {
1014 assert_eq!(ToolGuidanceProfile::resolve(32_000, 100, 1000, None, None), ToolGuidanceProfile::Minimal);
1015 assert_eq!(ToolGuidanceProfile::resolve(1_000_000, 100, 1000, None, Some(0.0)), ToolGuidanceProfile::Default);
1016 assert_eq!(ToolGuidanceProfile::resolve(1_000_000, 1001, 1000, None, None), ToolGuidanceProfile::Minimal);
1017 assert_eq!(
1018 ToolGuidanceProfile::resolve(1_000_000, 1000, 2000, Some(0.00001), Some(0.001)),
1019 ToolGuidanceProfile::Minimal
1020 );
1021 }
1022
1023 #[test]
1024 fn minimal_and_default_tool_guidance_snapshots() {
1025 let tools = vec![TOOL_READ_FILE.to_owned()];
1026 let minimal = generate_tool_guidelines_with_capabilities(
1027 &tools,
1028 None,
1029 ResolvedShellPromptProfile::UnixLike,
1030 ToolGuidanceProfile::Minimal,
1031 false,
1032 );
1033 assert_eq!(
1034 minimal,
1035 "\n\n## Active Tools\n- `verify: [skip Markdown lint if unavailable]`: report skipped; review diff/links without installing tools. Lint errors remain failures.\n- Capabilities: read-only. Analyze and search, but do not modify files or run shell commands.\n- Use available read-only repository tools for browsing; do not modify files."
1036 );
1037 let default = generate_tool_guidelines_with_capabilities(
1038 &tools,
1039 None,
1040 ResolvedShellPromptProfile::UnixLike,
1041 ToolGuidanceProfile::Default,
1042 false,
1043 );
1044 assert_eq!(
1045 default,
1046 "\n\n## Active Tools\n- `verify: [skip Markdown lint if unavailable]`: report skipped; review diff/links without installing tools. Lint errors remain failures.\n- Capabilities: read-only. Analyze and search, but do not modify files or run shell commands.\n- Use available read-only repository tools for browsing; do not modify files.\n- Batch independent read-only calls; use bounded `read_file` ranges, order dependencies, serialize mutations; narrow the range on `line_truncated`; prefer wider windows and reuse evidence over overlapping slices."
1047 );
1048 }
1049
1050 #[test]
1051 fn tool_presence_flags_record_decision_in_the_single_pass() {
1052 let with = ToolPresence::of(&["matrix".to_string(), "record_decision".to_string()]);
1053 assert!(with.matrix);
1054 assert!(with.record_decision);
1055 let without = ToolPresence::of(&["matrix".to_string()]);
1056 assert!(without.matrix);
1057 assert!(!without.record_decision);
1058 }
1059
1060 #[test]
1061 fn public_decision_guidance_is_optional_and_bounded() {
1062 for profile in [ToolGuidanceProfile::Minimal, ToolGuidanceProfile::Default] {
1063 let enabled = generate_tool_guidelines_with_capabilities(
1064 &[TOOL_READ_FILE.into(), "record_decision".into()],
1065 None,
1066 ResolvedShellPromptProfile::UnixLike,
1067 profile,
1068 false,
1069 );
1070 let disabled = generate_tool_guidelines_with_capabilities(
1071 &[TOOL_READ_FILE.into()],
1072 None,
1073 ResolvedShellPromptProfile::UnixLike,
1074 profile,
1075 false,
1076 );
1077 assert!(enabled.contains(PUBLIC_DECISION_GUIDANCE));
1078 assert!(!disabled.contains("record_decision"));
1079 assert!(enabled.len().saturating_sub(disabled.len()) < 300);
1080 assert!(!enabled.contains("chain of thought"));
1081 }
1082 }
1083
1084 #[test]
1085 fn tool_guidance_uses_actual_parallel_capabilities_for_three_families() {
1086 use crate::config::constants::models;
1087 use crate::llm::provider::LLMProvider;
1088 use crate::llm::providers::{AnthropicProvider, GeminiProvider, OpenAIProvider};
1089 let providers: [(Box<dyn LLMProvider>, &str); 3] = [
1090 (Box::new(OpenAIProvider::new("offline-fixture".into())), models::openai::DEFAULT_MODEL),
1091 (Box::new(AnthropicProvider::new("offline-fixture".into())), models::anthropic::DEFAULT_MODEL),
1092 (Box::new(GeminiProvider::new("offline-fixture".into())), models::google::DEFAULT_MODEL),
1093 ];
1094 for (provider, model) in providers {
1095 for profile in [ToolGuidanceProfile::Minimal, ToolGuidanceProfile::Default] {
1096 let parallel = provider.supports_parallel_tool_config(model);
1097 let text = generate_tool_guidelines_with_capabilities(
1098 &[TOOL_EXEC_COMMAND.to_owned()],
1099 None,
1100 ResolvedShellPromptProfile::UnixLike,
1101 profile,
1102 parallel,
1103 );
1104 assert_eq!(text.contains("tools in parallel"), parallel);
1105 let serial = generate_tool_guidelines_with_capabilities(
1106 &[TOOL_EXEC_COMMAND.to_owned()],
1107 None,
1108 ResolvedShellPromptProfile::UnixLike,
1109 profile,
1110 false,
1111 );
1112 assert!(!serial.contains("tools in parallel"));
1113 }
1114 }
1115 }
1116
1117 #[test]
1118 fn test_read_only_capability_detection() {
1119 let tools = vec![TOOL_CODE_SEARCH.to_string()];
1120 let guidelines = guidelines_for(&tools, None);
1121 assert!(guidelines.contains("Capabilities: read-only"));
1122 assert!(guidelines.contains("do not modify files"));
1123 }
1124
1125 #[test]
1126 fn test_tool_preference_guidance() {
1127 let tools = vec![TOOL_EXEC_COMMAND.to_string(), TOOL_CODE_SEARCH.to_string()];
1128 let guidelines = generate_tool_guidelines_for_profile(&tools, None, ResolvedShellPromptProfile::UnixLike, true);
1129 assert!(guidelines.contains("Advanced `code_search` takes `query`"));
1130 assert!(guidelines.contains("literal smart-case"));
1131 assert!(guidelines.contains("exact syntactic usages"));
1132 assert!(guidelines.contains("\"result_types\":[\"definition\"]"));
1133 assert!(guidelines.contains("Do not JSON-encode arrays or integers as strings"));
1134 assert!(guidelines.contains("omit unused filters"));
1135 assert!(guidelines.contains("path: \"\""));
1136 assert!(guidelines.contains("git diff -- <path>"));
1137 assert!(guidelines.contains("build tools"));
1138 assert!(guidelines.contains("test tools"));
1139 assert!(guidelines.contains("Prefer `code_search` over `rg`/`grep` for code"));
1143 assert!(guidelines.contains("Run fast checks before full builds"));
1145 assert!(!guidelines.contains("Completion is a checkpoint"));
1148 }
1149
1150 #[test]
1151 fn test_edit_workflow_guidance() {
1152 let tools = vec![TOOL_APPLY_PATCH.to_string()];
1153 let guidelines = guidelines_for(&tools, None);
1154 assert!(guidelines.contains("Use `apply_patch`"));
1155 assert!(guidelines.contains("patches small"));
1156 assert!(!guidelines.contains("verification resolved"));
1159 }
1160
1161 #[test]
1162 fn test_vt_code_guidance_omits_task_tracker() {
1163 let tools = vec![
1164 TOOL_EXEC_COMMAND.to_string(),
1165 TOOL_WRITE_STDIN.to_string(),
1166 TOOL_APPLY_PATCH.to_string(),
1167 ];
1168 let guidelines = generate_tool_guidelines_for_profile(&tools, None, ResolvedShellPromptProfile::UnixLike, true);
1169
1170 assert!(guidelines.contains("exec_command.cmd"));
1171 for command in ["ls", "rg", "find", "cat", "sed", "awk"] {
1172 assert!(
1173 guidelines.contains(&format!("`{command}`")),
1174 "{command} should be shown as an exec_command.cmd example"
1175 );
1176 }
1177 assert!(guidelines.contains("`write_stdin`"));
1178 assert!(guidelines.contains("At most three live background processes"));
1179 assert!(guidelines.contains("`apply_patch`"));
1180 assert!(!guidelines.contains("task_tracker"));
1181 assert!(!guidelines.contains("list_files"));
1182 assert!(!guidelines.contains("read_file"));
1183 }
1184
1185 #[test]
1186 fn task_tracker_guidance_explains_action_aware_indices() {
1187 let guidelines = generate_runtime_tool_guidelines_for_profile(
1188 &[TOOL_TASK_TRACKER.to_string()],
1189 true,
1190 ResolvedShellPromptProfile::UnixLike,
1191 );
1192
1193 assert!(guidelines.contains(&format!("\n{PLANNING_TASK_TRACKER_INDEX_LINE}")));
1194 assert!(PLANNING_TASK_TRACKER_INDEX_LINE.contains("positive flat indices"));
1195 assert!(PLANNING_TASK_TRACKER_INDEX_LINE.contains("(index 0 is invalid while planning)"));
1196 assert!(PLANNING_TASK_TRACKER_INDEX_LINE.contains("full checklist replacement"));
1197 assert!(PLANNING_TASK_TRACKER_INDEX_LINE.contains(r#"{"action":"list"}"#));
1198 assert!(PLANNING_TASK_TRACKER_INDEX_LINE.contains(r#"{"action":"update","index":1,"status":"in_progress"}"#));
1199 assert!(
1200 PLANNING_TASK_TRACKER_INDEX_LINE.contains(r#"{"action":"update","items":["[x] Done","[ ] Remaining"]}"#)
1201 );
1202 assert!(PLANNING_TASK_TRACKER_COMPACT_LINE.contains("never conflicting fields"));
1203 for line in [PLANNING_TASK_TRACKER_INDEX_LINE, PLANNING_TASK_TRACKER_COMPACT_LINE] {
1206 assert!(line.contains("index 0 is invalid while planning"), "{line}");
1207 assert!(!line.contains("reserved"), "{line}");
1208 assert!(!line.contains("index: 0"), "{line}");
1209 }
1210 }
1211
1212 #[test]
1213 fn unix_like_guidance_makes_command_reuse_explicit() {
1214 let tools = vec![TOOL_EXEC_COMMAND.to_string(), TOOL_WRITE_STDIN.to_string()];
1215 let guidelines = generate_tool_guidelines_for_profile(&tools, None, ResolvedShellPromptProfile::UnixLike, true);
1216
1217 assert!(guidelines.contains("one-shot `exec_command` calls"));
1218 assert!(guidelines.contains("`!!`, `!$`, `!ssh`, or `fc`"));
1219 assert!(guidelines.contains("write full command arguments explicitly"));
1220 assert!(guidelines.contains("conversation or tool results"));
1221 assert!(guidelines.contains("existing `session_id`"));
1222 assert!(guidelines.contains("background: true"));
1223 assert!(guidelines.contains("Bash `histverify`"));
1224 assert!(guidelines.contains("zsh `HIST_VERIFY`"));
1225 assert!(guidelines.contains("`Exec session resume:`"));
1228 assert!(guidelines.contains("prior turn ended mid-run"));
1229 assert!(!guidelines.contains("Prefer `code_search` over `rg`"));
1232 }
1233
1234 #[test]
1235 fn write_stdin_guidance_advertises_cross_turn_resume_hint() {
1236 let tools = vec![TOOL_WRITE_STDIN.to_string()];
1237 let default_guidance =
1238 generate_tool_guidelines_for_profile(&tools, None, ResolvedShellPromptProfile::UnixLike, true);
1239 assert!(default_guidance.contains("`Exec session resume:`"));
1240 assert!(default_guidance.contains("prior turn ended mid-run"));
1241 assert!(default_guidance.contains("if missing, recover prior output"));
1242 assert!(default_guidance.contains("rerun only for fresh results"));
1243
1244 let minimal = generate_tool_guidelines_with_capabilities(
1245 &tools,
1246 None,
1247 ResolvedShellPromptProfile::UnixLike,
1248 ToolGuidanceProfile::Minimal,
1249 false,
1250 );
1251 assert!(minimal.contains("`Exec session resume:`"));
1252 assert!(minimal.contains("prior turn ended mid-run"));
1253 assert!(minimal.contains("if missing, recover prior output"));
1254 }
1255
1256 #[test]
1257 fn powershell_guidance_uses_native_command_examples() {
1258 let tools = vec![
1259 TOOL_EXEC_COMMAND.to_string(),
1260 TOOL_CODE_SEARCH.to_string(),
1261 TOOL_APPLY_PATCH.to_string(),
1262 ];
1263 let guidelines =
1264 generate_tool_guidelines_for_profile(&tools, None, ResolvedShellPromptProfile::PowerShell, true);
1265
1266 assert!(guidelines.contains("native PowerShell commands"));
1267 assert!(guidelines.contains("`Get-ChildItem`"));
1268 assert!(guidelines.contains("`Select-String`"));
1269 assert!(guidelines.contains("native PowerShell syntax"));
1270 assert!(guidelines.contains("Prefer `code_search` over `Select-String` for code"));
1271 assert!(guidelines.contains("Advanced `code_search` takes `query`"));
1272 assert!(guidelines.contains("literal smart-case"));
1273 assert!(guidelines.contains("omit unused filters"));
1274 assert!(!guidelines.contains("`ls`, `rg`, `find`, `cat`, `sed`, and `awk`"));
1275 assert!(!guidelines.contains("shell history expansion"));
1276 assert!(!guidelines.contains("histverify"));
1277 assert!(!guidelines.contains("HIST_VERIFY"));
1278 }
1279
1280 #[test]
1281 fn shell_profile_prompt_keeps_policy_and_syntax_separate() {
1282 let unix = render_shell_profile_guidance(ResolvedShellPromptProfile::UnixLike);
1283 assert!(unix.contains("Active shell profile: `unix_like`"));
1284 assert!(unix.contains("does not rewrite GNU flags for macOS BSD tools"));
1285 assert!(unix.contains("controls prompt examples and expected command syntax only"));
1286 assert!(unix.contains("does not translate GNU-to-BSD"));
1287
1288 let powershell = render_shell_profile_guidance(ResolvedShellPromptProfile::PowerShell);
1289 assert!(powershell.contains("Active shell profile: `powershell`"));
1290 assert!(powershell.contains("WSL"));
1291 assert!(powershell.contains("Unix-like workflows"));
1292 assert!(powershell.contains("PowerShell-to-Unix"));
1293 }
1294
1295 #[test]
1296 fn test_harness_browse_tool_guidance() {
1297 let tools = vec![TOOL_LIST_FILES.to_string(), TOOL_READ_FILE.to_string()];
1298 let guidelines = guidelines_for(&tools, None);
1299 assert!(guidelines.contains("available read-only repository tools"));
1300 assert!(guidelines.contains("bounded `read_file` ranges"));
1301 assert!(!guidelines.contains("list_files"));
1302 assert!(!guidelines.contains("offset"));
1303 assert!(!guidelines.contains("per_page"));
1304 }
1305
1306 #[test]
1307 fn test_canonical_browse_tool_guidance_prefers_public_tools() {
1308 let tools = vec![
1309 TOOL_CODE_SEARCH.to_string(),
1310 TOOL_LIST_FILES.to_string(),
1311 "read_file".to_string(),
1312 ];
1313 let guidelines = guidelines_for(&tools, None);
1314 assert!(guidelines.contains("available read-only repository tools"));
1315 assert!(guidelines.contains("code_search"));
1316 assert!(guidelines.contains("bounded `read_file` ranges"));
1317 }
1318
1319 #[test]
1320 fn test_capability_basic_guidance() {
1321 let tools = vec![];
1322 let guidelines = guidelines_for(&tools, Some(CapabilityLevel::Basic));
1323 assert!(guidelines.contains("Capabilities: limited"));
1324 assert!(guidelines.contains("enable more capabilities"));
1325 }
1326
1327 #[test]
1328 fn test_capability_file_reading_guidance() {
1329 let tools = vec![TOOL_APPLY_PATCH.to_string()];
1330 let guidelines = guidelines_for(&tools, Some(CapabilityLevel::FileReading));
1331 assert!(guidelines.contains("Capabilities: read-only"));
1332 assert!(guidelines.contains("do not modify"));
1333 }
1334
1335 #[test]
1336 fn test_full_capabilities_no_special_guidance() {
1337 let tools = vec![
1338 TOOL_APPLY_PATCH.to_string(),
1339 TOOL_EXEC_COMMAND.to_string(),
1340 TOOL_CODE_SEARCH.to_string(),
1341 ];
1342 let guidelines = generate_tool_guidelines_for_profile(
1343 &tools,
1344 Some(CapabilityLevel::Editing),
1345 ResolvedShellPromptProfile::UnixLike,
1346 true,
1347 );
1348
1349 assert!(!guidelines.contains("Capabilities: limited"));
1350 assert!(!guidelines.contains("Capabilities: read-only"));
1351 }
1352
1353 #[test]
1354 fn test_empty_tools_shows_read_only_capabilities() {
1355 let tools = vec![];
1356 let guidelines = guidelines_for(&tools, None);
1357 assert!(guidelines.contains("Capabilities: read-only"));
1358 }
1359
1360 #[test]
1361 fn test_planning_workflow_guidance_keeps_verification_open() {
1362 let tools = vec![
1363 TOOL_EXEC_COMMAND.to_string(),
1364 TOOL_TASK_TRACKER.to_string(),
1365 TOOL_CODE_SEARCH.to_string(),
1366 ];
1367 let guidelines =
1368 generate_runtime_tool_guidelines_for_profile(&tools, true, ResolvedShellPromptProfile::UnixLike);
1369 assert!(guidelines.contains("Keep `task_tracker` updated"));
1370 assert!(guidelines.contains("blockers and verification open"));
1371 }
1372
1373 #[test]
1374 fn tool_presence_of_sets_each_flag_in_one_pass() {
1375 let tools = vec![
1376 TOOL_EXEC_COMMAND.to_string(),
1377 TOOL_WRITE_STDIN.to_string(),
1378 TOOL_CODE_SEARCH.to_string(),
1379 TOOL_READ_FILE.to_string(),
1380 TOOL_LIST_FILES.to_string(),
1381 TOOL_APPLY_PATCH.to_string(),
1382 TOOL_REQUEST_USER_INPUT.to_string(),
1383 TOOL_TASK_TRACKER.to_string(),
1384 TOOL_START_PLANNING.to_string(),
1385 "matrix".to_string(),
1386 "unknown_tool".to_string(),
1387 ];
1388 let presence = ToolPresence::of(&tools);
1389 assert!(presence.exec);
1390 assert!(presence.write_stdin);
1391 assert!(presence.code_search);
1392 assert!(presence.read_file);
1393 assert!(presence.list_files);
1394 assert!(presence.apply_patch);
1395 assert!(presence.request_user_input);
1396 assert!(presence.task_tracker);
1397 assert!(presence.start_planning);
1398 assert!(presence.matrix);
1399
1400 let empty = ToolPresence::of(&[]);
1401 assert!(!empty.exec);
1402 assert!(!empty.write_stdin);
1403 assert!(!empty.code_search);
1404 assert!(!empty.read_file);
1405 assert!(!empty.list_files);
1406 assert!(!empty.apply_patch);
1407 assert!(!empty.request_user_input);
1408 assert!(!empty.task_tracker);
1409 assert!(!empty.start_planning);
1410 assert!(!empty.matrix);
1411 }
1412
1413 #[test]
1414 fn test_capability_inference_precedence() {
1415 let tools = vec![TOOL_APPLY_PATCH.to_string(), TOOL_CODE_SEARCH.to_string()];
1416 assert_eq!(infer_capability_level(&tools), CapabilityLevel::CodeSearch);
1417
1418 let tools = vec![TOOL_EXEC_COMMAND.to_string(), TOOL_APPLY_PATCH.to_string()];
1419 assert_eq!(infer_capability_level(&tools), CapabilityLevel::Editing);
1420 }
1421
1422 #[test]
1423 fn test_capability_inference_variants() {
1424 let tools = vec![TOOL_APPLY_PATCH.to_string()];
1425 assert_eq!(infer_capability_level(&tools), CapabilityLevel::Editing);
1426
1427 let tools = vec![TOOL_EXEC_COMMAND.to_string()];
1428 assert_eq!(infer_capability_level(&tools), CapabilityLevel::Bash);
1429
1430 let tools = vec![TOOL_CODE_SEARCH.to_string()];
1431 assert_eq!(infer_capability_level(&tools), CapabilityLevel::CodeSearch);
1432
1433 let tools = vec![TOOL_LIST_FILES.to_string()];
1434 assert_eq!(infer_capability_level(&tools), CapabilityLevel::FileListing);
1435
1436 let tools = vec!["read_file".to_string()];
1437 assert_eq!(infer_capability_level(&tools), CapabilityLevel::FileReading);
1438
1439 let tools = vec!["unknown_tool".to_string()];
1440 assert_eq!(infer_capability_level(&tools), CapabilityLevel::Basic);
1441 }
1442
1443 #[test]
1444 fn test_guidelines_stay_compact() {
1445 let tools = vec![
1446 TOOL_EXEC_COMMAND.to_string(),
1447 TOOL_CODE_SEARCH.to_string(),
1448 "read_file".to_string(),
1449 TOOL_LIST_FILES.to_string(),
1450 "apply_patch".to_string(),
1451 ];
1452 let guidelines = generate_tool_guidelines_for_profile(&tools, None, ResolvedShellPromptProfile::UnixLike, true);
1453 assert!(guidelines.contains("Batch independent read-only calls"));
1454 assert!(guidelines.contains("code_search"));
1455 assert!(guidelines.contains("static read-only filtering pipelines use fail-closed `pipefail`"));
1458 assert!(guidelines.contains("Only terminal exit 0 clears verification"));
1459 assert!(guidelines.contains("Recheck after `--fix`"));
1460 assert!(guidelines.contains("dynamic syntax, mutating tails, `;`, and `||` do not qualify"));
1461 assert!(!guidelines.contains("elided"));
1462 assert!(guidelines.contains("max_output_tokens"));
1463 assert!(guidelines.contains("Build and Auto share tools and safety gates"));
1464 let approx_tokens = vtcode_commons::estimate_tokens(&guidelines);
1465 assert!(approx_tokens < 640, "got ~{approx_tokens} tokens");
1476 }
1477
1478 #[test]
1479 fn deferred_tools_section_caps_groups_and_truncates_descriptions() {
1480 use crate::llm::provider::ToolNamespace;
1481
1482 fn deferred_mcp_tool(server: &str, tool: &str, description: &str) -> ToolDefinition {
1483 let mut definition = ToolDefinition::function(
1484 format!("mcp__{server}__{tool}"),
1485 "deferred".to_string(),
1486 serde_json::json!({"type": "object"}),
1487 );
1488 definition.namespace = Some(ToolNamespace {
1489 name: server.to_string(),
1490 description: description.to_string(),
1491 });
1492 definition.defer_loading = Some(true);
1493 definition
1494 }
1495
1496 let mut tools = Vec::new();
1497 for index in 0..10 {
1498 tools.push(deferred_mcp_tool(&format!("server-{index:02}"), "search", "Tools provided by MCP server"));
1499 }
1500 tools.push(deferred_mcp_tool("server-long", "search", &"d".repeat(500)));
1502
1503 let mut prompt = "Base prompt".to_string();
1504 append_deferred_tools_prompt_section(&mut prompt, &tools);
1505
1506 assert!(prompt.contains("[Deferred Tools]"));
1507 assert!(prompt.contains("server-00 (1 tools)"));
1508 assert!(prompt.contains("server-07 (1 tools)"));
1509 assert!(!prompt.contains("server-08 (1 tools)"), "groups past the cap must collapse into overflow");
1510 assert!(!prompt.contains("server-09 (1 tools)"));
1511 assert!(
1512 prompt.contains("(+3 more deferred groups available"),
1513 "10 named groups + 1 long group over the 8-group cap overflows by 3"
1514 );
1515 assert!(!prompt.contains(&"d".repeat(121)), "group descriptions must stay truncated");
1516 assert!(prompt.contains("Use `search_tools` to find a deferred capability"));
1517
1518 append_deferred_tools_prompt_section(&mut prompt, &tools);
1520 assert_eq!(prompt.matches("[Deferred Tools]").count(), 1);
1521 }
1522
1523 #[test]
1524 fn deferred_tools_section_omits_empty_and_fully_loaded_groups() {
1525 let mut prompt = "Base prompt".to_string();
1526 append_deferred_tools_prompt_section(&mut prompt, &[]);
1527 assert!(!prompt.contains("[Deferred Tools]"));
1528
1529 let loaded = ToolDefinition::function(
1530 "exec_command".to_string(),
1531 "Shell".to_string(),
1532 serde_json::json!({"type": "object"}),
1533 );
1534 let mut prompt = "Base prompt".to_string();
1535 append_deferred_tools_prompt_section(&mut prompt, &[loaded]);
1536 assert!(!prompt.contains("[Deferred Tools]"));
1537 }
1538
1539 #[test]
1540 fn truncate_deferred_description_keeps_boundaries_and_char_safety() {
1541 let short = "Tools provided by MCP server";
1544 assert_eq!(truncate_deferred_description(short), short);
1545
1546 let empty = "";
1547 assert_eq!(truncate_deferred_description(empty), "");
1548
1549 let exact: String = "a".repeat(DEFERRED_TOOLS_MAX_DESC_CHARS);
1550 assert_eq!(truncate_deferred_description(&exact), exact);
1551
1552 let one_over: String = "a".repeat(DEFERRED_TOOLS_MAX_DESC_CHARS + 1);
1553 let truncated = truncate_deferred_description(&one_over);
1554 assert!(truncated.ends_with("..."));
1555 assert!(!truncated.contains(&"a".repeat(DEFERRED_TOOLS_MAX_DESC_CHARS + 1)));
1556 assert_eq!(truncated.chars().count(), DEFERRED_TOOLS_MAX_DESC_CHARS + 3);
1557
1558 let unicode_exact = format!("{}🚀", "a".repeat(DEFERRED_TOOLS_MAX_DESC_CHARS - 1));
1560 assert_eq!(unicode_exact.chars().count(), DEFERRED_TOOLS_MAX_DESC_CHARS);
1561 assert_eq!(truncate_deferred_description(&unicode_exact), unicode_exact);
1562
1563 let unicode_over = format!("{}🚀b", "a".repeat(DEFERRED_TOOLS_MAX_DESC_CHARS - 1));
1564 let unicode_truncated = truncate_deferred_description(&unicode_over);
1565 assert!(unicode_truncated.ends_with("..."));
1566 assert!(unicode_truncated.contains("🚀"));
1567
1568 let padded = format!("{} {}", "a".repeat(DEFERRED_TOOLS_MAX_DESC_CHARS - 4), "b".repeat(10));
1570 let padded_truncated = truncate_deferred_description(&padded);
1571 assert!(padded_truncated.ends_with("..."));
1572 assert!(!padded_truncated.contains(" ..."));
1573 }
1574
1575 #[test]
1576 fn test_parallel_tool_call_guidance() {
1577 let tools = vec![
1578 TOOL_EXEC_COMMAND.to_string(),
1579 TOOL_CODE_SEARCH.to_string(),
1580 TOOL_APPLY_PATCH.to_string(),
1581 ];
1582 let guidelines = generate_tool_guidelines_for_profile(&tools, None, ResolvedShellPromptProfile::UnixLike, true);
1583 assert!(guidelines.contains("parallel"), "Should include parallel tool call guidance");
1584 assert!(guidelines.contains("inputs do not depend"), "Should mention independent inputs");
1585 }
1586
1587 #[test]
1588 fn test_read_only_batching_guidance_is_explicit() {
1589 let tools = vec![
1590 TOOL_CODE_SEARCH.to_string(),
1591 TOOL_READ_FILE.to_string(),
1592 TOOL_LIST_FILES.to_string(),
1593 ];
1594 let guidelines = generate_tool_guidelines_for_profile(&tools, None, ResolvedShellPromptProfile::UnixLike, true);
1595
1596 assert!(guidelines.contains("Batch independent read-only calls"));
1597 assert!(guidelines.contains("`read_file` ranges"));
1598 assert!(guidelines.contains("serialize mutations"));
1599 assert!(!guidelines.contains("Only terminal exit 0 clears verification"));
1602 }
1603
1604 #[test]
1605 fn execution_agents_can_suggest_planning_for_demanding_tasks() {
1606 let tools = vec![TOOL_START_PLANNING.to_string(), TOOL_EXEC_COMMAND.to_string()];
1607 let guidelines = generate_tool_guidelines_for_profile(&tools, None, ResolvedShellPromptProfile::UnixLike, true);
1608
1609 assert_eq!(guidelines.matches(START_PLANNING_GUIDANCE_LINE).count(), 1);
1610 let minimal = generate_tool_guidelines_with_capabilities(
1611 &tools,
1612 None,
1613 ResolvedShellPromptProfile::UnixLike,
1614 ToolGuidanceProfile::Minimal,
1615 false,
1616 );
1617 assert_eq!(minimal.matches(START_PLANNING_GUIDANCE_LINE).count(), 1);
1618 let without = generate_tool_guidelines_for_profile(
1620 &[TOOL_EXEC_COMMAND.to_string()],
1621 None,
1622 ResolvedShellPromptProfile::UnixLike,
1623 true,
1624 );
1625 assert!(!without.contains("start_planning"));
1626 }
1627
1628 #[test]
1629 fn markdown_validation_is_optional_in_planning_and_execution() {
1630 let tools = vec![TOOL_EXEC_COMMAND.to_string(), TOOL_READ_FILE.to_string()];
1631 for planning in [false, true] {
1632 let guidance =
1633 generate_runtime_tool_guidelines_for_profile(&tools, planning, ResolvedShellPromptProfile::UnixLike);
1634 assert_eq!(guidance.matches(OPTIONAL_MARKDOWN_VALIDATION_GUIDANCE).count(), 1);
1635 assert!(guidance.contains("report skipped"));
1636 assert!(guidance.contains("without installing tools"));
1637 assert!(guidance.contains("Lint errors remain failures"));
1638 }
1639 }
1640
1641 #[test]
1642 fn planning_workflow_runtime_guidance_keeps_exec_read_only() {
1643 let tools = vec![
1644 TOOL_APPLY_PATCH.to_string(),
1645 TOOL_EXEC_COMMAND.to_string(),
1646 TOOL_CODE_SEARCH.to_string(),
1647 ];
1648 let guidelines =
1649 generate_runtime_tool_guidelines_for_profile(&tools, true, ResolvedShellPromptProfile::UnixLike);
1650
1651 assert!(guidelines.contains("Planning workflow active"));
1652 assert!(guidelines.contains("`exec_command` only for read-only verification"));
1653 assert!(!guidelines.contains("<proposed_plan>"));
1654 assert!(!guidelines.contains("Every implementation step"));
1655 assert!(guidelines.contains("omit unused filters"));
1656 assert!(!guidelines.contains("Inspect before edit"));
1657 }
1658
1659 #[test]
1660 fn minimal_planning_addendum_prefers_read_tools_over_shell_reads() {
1661 let tail = "start git history with `git log --oneline`";
1666 let with_readers = ToolPresence::of(&[TOOL_READ_FILE.to_string(), TOOL_CODE_SEARCH.to_string()]);
1667 let mut guidance = String::new();
1668 append_minimal_planning_addendum(&mut guidance, &with_readers);
1669 assert!(guidance.contains("prefer `read_file`/`code_search` over `exec_command` shell reads"));
1670 assert!(guidance.contains(tail));
1671
1672 let without_readers = ToolPresence::of(&[TOOL_EXEC_COMMAND.to_string()]);
1673 let mut bare = String::new();
1674 append_minimal_planning_addendum(&mut bare, &without_readers);
1675 assert!(!bare.contains("prefer `"));
1676 assert!(!bare.contains("over `exec_command` shell reads"));
1677 assert!(bare.contains(tail));
1678 }
1679
1680 #[test]
1681 fn runtime_tool_guidance_uses_explicit_powershell_profile() {
1682 let tools = vec![
1683 TOOL_APPLY_PATCH.to_string(),
1684 TOOL_EXEC_COMMAND.to_string(),
1685 TOOL_CODE_SEARCH.to_string(),
1686 ];
1687 let guidelines =
1688 generate_runtime_tool_guidelines_for_profile(&tools, false, ResolvedShellPromptProfile::PowerShell);
1689
1690 assert!(guidelines.contains("native PowerShell commands"));
1691 assert!(guidelines.contains("`Get-ChildItem`"));
1692 assert!(guidelines.contains("`Select-String`"));
1693 assert!(guidelines.contains("native PowerShell syntax"));
1694 assert!(!guidelines.contains("`ls`, `rg`, `find`, `cat`, `sed`, and `awk`"));
1695 }
1696
1697 #[test]
1698 fn runtime_tool_guidance_uses_explicit_unix_like_profile() {
1699 let tools = vec![
1700 TOOL_APPLY_PATCH.to_string(),
1701 TOOL_EXEC_COMMAND.to_string(),
1702 TOOL_CODE_SEARCH.to_string(),
1703 ];
1704 let guidelines =
1705 generate_runtime_tool_guidelines_for_profile(&tools, false, ResolvedShellPromptProfile::UnixLike);
1706
1707 assert!(guidelines.contains("`ls`, `find`, `cat`, `sed`, and `awk` for repository browsing"));
1708 assert!(guidelines.contains("Prefer `code_search` over `rg`/`grep` for code"));
1709 assert!(guidelines.contains("Advanced `code_search` takes `query`"));
1710 assert!(guidelines.contains("literal smart-case"));
1711 assert!(guidelines.contains("shell-only tasks"));
1712 assert!(!guidelines.contains("native PowerShell commands"));
1713 assert!(!guidelines.contains("`Get-ChildItem`"));
1714 }
1715
1716 #[test]
1717 fn shell_browse_guidance_lists_rg_only_without_code_search() {
1718 let without_search = shell_browse_guidance(ResolvedShellPromptProfile::UnixLike, false);
1722 assert!(without_search.contains("`ls`, `rg`, `find`, `cat`, `sed`, and `awk` for repository browsing"));
1723 assert!(!without_search.contains("Prefer `code_search`"));
1724
1725 let with_search = shell_browse_guidance(ResolvedShellPromptProfile::UnixLike, true);
1726 assert!(with_search.contains("`ls`, `find`, `cat`, `sed`, and `awk` for repository browsing"));
1727 assert!(!with_search.contains("`ls`, `rg`, `find`"));
1728 assert!(with_search.contains("Prefer `code_search` over `rg`/`grep` for code"));
1729 }
1730
1731 #[test]
1732 fn runtime_tool_prompt_sections_use_explicit_profile_for_active_tools() {
1733 let mut powershell_prompt = "Base prompt".to_string();
1734 let mut unix_prompt = "Base prompt".to_string();
1735 let snapshot = SessionToolCatalogSnapshot::new(
1736 7,
1737 9,
1738 false,
1739 false,
1740 Some(std::sync::Arc::new(vec![
1741 ToolDefinition::function(
1742 TOOL_EXEC_COMMAND.to_string(),
1743 "Shell".to_string(),
1744 serde_json::json!({"type": "object"}),
1745 ),
1746 ToolDefinition::function(
1747 TOOL_CODE_SEARCH.to_string(),
1748 "Bounded source search".to_string(),
1749 serde_json::json!({"type": "object"}),
1750 ),
1751 ])),
1752 false,
1753 );
1754
1755 append_runtime_tool_prompt_sections_for_profile(
1756 &mut powershell_prompt,
1757 &snapshot,
1758 false,
1759 ResolvedShellPromptProfile::PowerShell,
1760 );
1761 append_runtime_tool_prompt_sections_for_profile(
1762 &mut unix_prompt,
1763 &snapshot,
1764 false,
1765 ResolvedShellPromptProfile::UnixLike,
1766 );
1767
1768 assert!(powershell_prompt.contains("## Active Tools"));
1769 assert!(powershell_prompt.contains("`Get-ChildItem`"));
1770 assert!(powershell_prompt.contains("`Select-String`"));
1771 assert!(!powershell_prompt.contains("`ls`, `rg`, `find`, `cat`, `sed`, and `awk`"));
1772
1773 assert!(unix_prompt.contains("## Active Tools"));
1774 assert!(unix_prompt.contains("`ls`, `find`, `cat`, `sed`, and `awk` for repository browsing"));
1775 assert!(unix_prompt.contains("Prefer `code_search` over `rg`/`grep` for code"));
1776 assert!(unix_prompt.contains("Advanced `code_search` takes `query`"));
1777 assert!(unix_prompt.contains("literal smart-case"));
1778 assert!(!unix_prompt.contains("`Get-ChildItem`"));
1779 }
1780
1781 #[test]
1782 fn runtime_tool_prompt_sections_include_catalog_metadata() {
1783 let mut prompt = "Base prompt".to_string();
1784 let snapshot = SessionToolCatalogSnapshot::new(
1785 7,
1786 9,
1787 true,
1788 false,
1789 Some(std::sync::Arc::new(vec![
1790 ToolDefinition::function(
1791 TOOL_EXEC_COMMAND.to_string(),
1792 "Search".to_string(),
1793 serde_json::json!({"type": "object"}),
1794 ),
1795 ToolDefinition::function(
1796 TOOL_APPLY_PATCH.to_string(),
1797 "File".to_string(),
1798 serde_json::json!({"type": "object"}),
1799 ),
1800 ])),
1801 false,
1802 );
1803
1804 append_runtime_tool_prompt_sections_for_profile(
1805 &mut prompt,
1806 &snapshot,
1807 true,
1808 ShellPromptProfile::Auto.resolve_for_current_platform(),
1809 );
1810
1811 assert!(prompt.contains("## Active Tools"));
1812 assert!(prompt.contains("[Runtime Tool Catalog]"));
1813 assert!(prompt.contains("catalog_tools: 2"));
1814 assert!(prompt.contains("currently_available_tools: exec_command, apply_patch"));
1815 assert!(prompt.contains("request_user_input_enabled: false"));
1816 }
1817
1818 #[test]
1819 fn runtime_tool_prompt_sections_replace_existing_runtime_sections() {
1820 let mut prompt = "Base prompt".to_string();
1821 let first = SessionToolCatalogSnapshot::new(
1822 1,
1823 2,
1824 false,
1825 false,
1826 Some(std::sync::Arc::new(vec![ToolDefinition::function(
1827 TOOL_EXEC_COMMAND.to_string(),
1828 "Search".to_string(),
1829 serde_json::json!({"type": "object"}),
1830 )])),
1831 false,
1832 );
1833 let second = SessionToolCatalogSnapshot::new(
1834 7,
1835 9,
1836 true,
1837 true,
1838 Some(std::sync::Arc::new(vec![ToolDefinition::function(
1839 TOOL_APPLY_PATCH.to_string(),
1840 "File".to_string(),
1841 serde_json::json!({"type": "object"}),
1842 )])),
1843 false,
1844 );
1845
1846 let shell = ShellPromptProfile::Auto.resolve_for_current_platform();
1847 append_runtime_tool_prompt_sections_for_profile(&mut prompt, &first, true, shell);
1848 append_runtime_tool_prompt_sections_for_profile(&mut prompt, &second, true, shell);
1849
1850 assert_eq!(prompt.matches("## Active Tools").count(), 1);
1851 assert_eq!(prompt.matches("[Runtime Tool Catalog]").count(), 1);
1852 assert!(prompt.contains("version: 7"));
1853 assert!(!prompt.contains("version: 1"));
1854 assert!(prompt.contains("request_user_input_enabled: true"));
1855 assert!(!prompt.contains("request_user_input_enabled: false"));
1856 }
1857}