@@ -12,6 +12,29 @@ const ASK_PROMPT_TEMPLATE: &str = include_str!("ask_prompt.txt");
1212const CODING_PROMPT_TEMPLATE : & str = include_str ! ( "coding_prompt.txt" ) ;
1313const PLAN_PROMPT_TEMPLATE : & str = include_str ! ( "plan_prompt.txt" ) ;
1414
15+ /// Injected after the static body when a context-engine index is available.
16+ /// The base templates advertise only grep/glob and instruct the model to "use
17+ /// glob and grep to find what you need", so without this block models never reach
18+ /// for the `codebase_search`/`codebase_graph` tools (registered by
19+ /// `ToolRegistry::for_mode`) even though they're more capable. This block
20+ /// advertises the index tools and steers exploration to `codebase_search` first.
21+ const INDEX_PROMPT_BLOCK : & str = "# Codebase Index (semantic search — PREFER THIS for finding code)\n \
22+ \n \
23+ This project is indexed. Two extra tools are available and are faster and more accurate \
24+ than grep/glob for locating code:\n \
25+ \n \
26+ - `codebase_search` — semantic, AI-ranked search across the whole codebase. Handles \
27+ conceptual queries (e.g. \" color parsing\" , \" rate limiting logic\" ) and finds relevant \
28+ code even when you don't know the exact symbol or string.\n \
29+ - `codebase_graph` — call/dependency graph: find a function's callers, callees, \
30+ definitions, and references.\n \
31+ \n \
32+ Exploration workflow (this OVERRIDES the grep/glob guidance in the sections below): when \
33+ you need to find where something is implemented or understand how a feature works, call \
34+ `codebase_search` FIRST (and `codebase_graph` for relationships), then `read` the files it \
35+ points to. Use `grep`/`glob` only for exact-string matches, or if the index tool reports \
36+ it is unavailable.\n ";
37+
1538/// One block of the system prompt. Anthropic sends these in order; each block
1639/// can independently carry a `cache_control` marker.
1740#[ derive( Debug , Clone ) ]
@@ -45,6 +68,7 @@ pub fn build_system_prompt(
4568 project_note : Option < & str > ,
4669 skills : Option < & SkillRegistry > ,
4770 subagents : Option < & SubagentRegistry > ,
71+ context_engine_enabled : bool ,
4872) -> Vec < SystemBlock > {
4973 let template = match mode {
5074 ToolMode :: Ask => ASK_PROMPT_TEMPLATE ,
@@ -74,6 +98,15 @@ pub fn build_system_prompt(
7498 cache_control: Some ( CacheControl :: ephemeral( ) ) ,
7599 } ] ;
76100
101+ // Context-engine index guidance — only when an index is available for this run.
102+ // Its own ephemeral breakpoint so toggling the engine invalidates just this block.
103+ if context_engine_enabled {
104+ blocks. push ( SystemBlock {
105+ text : INDEX_PROMPT_BLOCK . to_string ( ) ,
106+ cache_control : Some ( CacheControl :: ephemeral ( ) ) ,
107+ } ) ;
108+ }
109+
77110 // Skills + Subagents share ONE ephemeral breakpoint. Toggling either
78111 // invalidates the combined block once (not twice).
79112 let skills_entries = skills. map ( |r| r. list_for_prompt ( ) ) . unwrap_or_default ( ) ;
@@ -155,7 +188,7 @@ mod tests {
155188 #[ test]
156189 fn test_build_system_prompt_splits_into_two_blocks ( ) {
157190 for mode in [ ToolMode :: Ask , ToolMode :: Coding , ToolMode :: Plan ] {
158- let blocks = build_system_prompt ( mode, & PathBuf :: from ( "/p" ) , Some ( "main" ) , None , None , None ) ;
191+ let blocks = build_system_prompt ( mode, & PathBuf :: from ( "/p" ) , Some ( "main" ) , None , None , None , false ) ;
159192 assert_eq ! ( blocks. len( ) , 2 , "{:?}: expected exactly 2 blocks" , mode) ;
160193 assert ! ( blocks[ 0 ] . cache_control. is_some( ) , "{:?}: block 0 must be cached" , mode) ;
161194 assert ! ( blocks[ 1 ] . cache_control. is_none( ) , "{:?}: block 1 must NOT be cached" , mode) ;
@@ -173,6 +206,7 @@ mod tests {
173206 None ,
174207 Some ( & registry) ,
175208 None ,
209+ false ,
176210 ) ;
177211 assert_eq ! ( blocks. len( ) , 3 , "{:?}: expected 3 blocks with skills" , mode) ;
178212 assert ! ( blocks[ 0 ] . cache_control. is_some( ) , "{:?}: static body cached" , mode) ;
@@ -193,15 +227,32 @@ mod tests {
193227 None ,
194228 Some ( & empty) ,
195229 None ,
230+ false ,
196231 ) ;
197232 assert_eq ! ( blocks. len( ) , 2 , "empty registry must not add a block" ) ;
198233 }
199234
235+ #[ test]
236+ fn test_index_block_inserted_only_when_context_engine_enabled ( ) {
237+ for mode in [ ToolMode :: Ask , ToolMode :: Coding , ToolMode :: Plan ] {
238+ let on = build_system_prompt ( mode, & PathBuf :: from ( "/p" ) , Some ( "main" ) , None , None , None , true ) ;
239+ assert ! (
240+ on. iter( ) . any( |b| b. text. contains( "codebase_search" ) ) ,
241+ "{:?}: index block must advertise codebase_search when engine enabled" , mode
242+ ) ;
243+ let off = build_system_prompt ( mode, & PathBuf :: from ( "/p" ) , Some ( "main" ) , None , None , None , false ) ;
244+ assert ! (
245+ !off. iter( ) . any( |b| b. text. contains( "codebase_search" ) ) ,
246+ "{:?}: no index block when engine disabled" , mode
247+ ) ;
248+ }
249+ }
250+
200251 #[ test]
201252 fn test_static_body_excludes_date ( ) {
202253 let today = chrono:: Local :: now ( ) . format ( "%Y-%m-%d" ) . to_string ( ) ;
203254 for mode in [ ToolMode :: Ask , ToolMode :: Coding , ToolMode :: Plan ] {
204- let blocks = build_system_prompt ( mode, & PathBuf :: from ( "/p" ) , Some ( "main" ) , None , None , None ) ;
255+ let blocks = build_system_prompt ( mode, & PathBuf :: from ( "/p" ) , Some ( "main" ) , None , None , None , false ) ;
205256 assert ! (
206257 !blocks[ 0 ] . text. contains( & today) ,
207258 "{:?}: static body must not contain today's date (would invalidate cache daily)" ,
@@ -214,7 +265,7 @@ mod tests {
214265 fn test_env_block_contains_date_and_working_dir ( ) {
215266 let today = chrono:: Local :: now ( ) . format ( "%Y-%m-%d" ) . to_string ( ) ;
216267 for mode in [ ToolMode :: Ask , ToolMode :: Coding , ToolMode :: Plan ] {
217- let blocks = build_system_prompt ( mode, & PathBuf :: from ( "/home/user/project" ) , Some ( "main" ) , None , None , None ) ;
268+ let blocks = build_system_prompt ( mode, & PathBuf :: from ( "/home/user/project" ) , Some ( "main" ) , None , None , None , false ) ;
218269 assert ! ( blocks[ 1 ] . text. contains( & today) , "{:?}: env must contain date" , mode) ;
219270 assert ! ( blocks[ 1 ] . text. contains( "/home/user/project" ) , "{:?}: env must contain working_dir" , mode) ;
220271 assert ! ( blocks[ 1 ] . text. contains( "main" ) , "{:?}: env must contain branch when provided" , mode) ;
@@ -226,35 +277,35 @@ mod tests {
226277 #[ test]
227278 fn test_working_dir_injected_all_modes ( ) {
228279 for mode in [ ToolMode :: Ask , ToolMode :: Coding , ToolMode :: Plan ] {
229- let prompt = joined ( & build_system_prompt ( mode, & PathBuf :: from ( "/home/user/project" ) , None , None , None , None ) ) ;
280+ let prompt = joined ( & build_system_prompt ( mode, & PathBuf :: from ( "/home/user/project" ) , None , None , None , None , false ) ) ;
230281 assert ! ( prompt. contains( "/home/user/project" ) , "Mode {:?} missing working_dir" , mode) ;
231282 assert ! ( !prompt. contains( "{{working_dir}}" ) , "Mode {:?} has unresolved placeholder" , mode) ;
232283 }
233284 }
234285
235286 #[ test]
236287 fn test_branch_injected ( ) {
237- let prompt = joined ( & build_system_prompt ( ToolMode :: Coding , & PathBuf :: from ( "/tmp" ) , Some ( "feature/login" ) , None , None , None ) ) ;
288+ let prompt = joined ( & build_system_prompt ( ToolMode :: Coding , & PathBuf :: from ( "/tmp" ) , Some ( "feature/login" ) , None , None , None , false ) ) ;
238289 assert ! ( prompt. contains( "feature/login" ) ) ;
239290 }
240291
241292 #[ test]
242293 fn test_branch_omitted_when_none ( ) {
243- let prompt = joined ( & build_system_prompt ( ToolMode :: Ask , & PathBuf :: from ( "/tmp" ) , None , None , None , None ) ) ;
294+ let prompt = joined ( & build_system_prompt ( ToolMode :: Ask , & PathBuf :: from ( "/tmp" ) , None , None , None , None , false ) ) ;
244295 assert ! ( !prompt. contains( "Git branch" ) ) ;
245296 }
246297
247298 #[ test]
248299 fn test_date_injected ( ) {
249- let prompt = joined ( & build_system_prompt ( ToolMode :: Ask , & PathBuf :: from ( "/tmp" ) , None , None , None , None ) ) ;
300+ let prompt = joined ( & build_system_prompt ( ToolMode :: Ask , & PathBuf :: from ( "/tmp" ) , None , None , None , None , false ) ) ;
250301 let today = chrono:: Local :: now ( ) . format ( "%Y-%m-%d" ) . to_string ( ) ;
251302 assert ! ( prompt. contains( & today) ) ;
252303 }
253304
254305 #[ test]
255306 fn test_os_arch_injected ( ) {
256307 for mode in [ ToolMode :: Ask , ToolMode :: Coding , ToolMode :: Plan ] {
257- let prompt = joined ( & build_system_prompt ( mode, & PathBuf :: from ( "/tmp" ) , None , None , None , None ) ) ;
308+ let prompt = joined ( & build_system_prompt ( mode, & PathBuf :: from ( "/tmp" ) , None , None , None , None , false ) ) ;
258309 assert ! ( prompt. contains( std:: env:: consts:: OS ) , "Mode {:?} missing OS" , mode) ;
259310 assert ! ( prompt. contains( std:: env:: consts:: ARCH ) , "Mode {:?} missing ARCH" , mode) ;
260311 }
@@ -263,7 +314,7 @@ mod tests {
263314 #[ test]
264315 fn test_no_unresolved_placeholders ( ) {
265316 for mode in [ ToolMode :: Ask , ToolMode :: Coding , ToolMode :: Plan ] {
266- let prompt = joined ( & build_system_prompt ( mode, & PathBuf :: from ( "/tmp" ) , Some ( "main" ) , None , None , None ) ) ;
317+ let prompt = joined ( & build_system_prompt ( mode, & PathBuf :: from ( "/tmp" ) , Some ( "main" ) , None , None , None , false ) ) ;
267318 assert ! ( !prompt. contains( "{{" ) , "Mode {:?} has unresolved placeholder: {}" , mode,
268319 prompt. find( "{{" ) . map( |i| & prompt[ i..( i+30 ) . min( prompt. len( ) ) ] ) . unwrap_or( "" ) ) ;
269320 }
0 commit comments