Skip to content

Commit bd712a3

Browse files
authored
Merge pull request #133 from TransformerOptimus/fix/agent-index-prompt
fix(agent): add context-engine tools in the system prompt
2 parents ff09054 + 395496b commit bd712a3

5 files changed

Lines changed: 64 additions & 10 deletions

File tree

apps/desktop/src-tauri/src/agent_bridge/commands.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,7 @@ fn build_agent_config(
732732
project_note,
733733
config.skills.as_deref(),
734734
config.subagents.as_deref(),
735+
config.context_engine.is_some(),
735736
));
736737
config
737738
}

crates/agent/src/agent/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ pub fn spawn_agent(
6464
None,
6565
config.skills.as_deref(),
6666
config.subagents.as_deref(),
67+
config.context_engine.is_some(),
6768
));
6869
}
6970

crates/agent/src/agent/prompt.rs

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,29 @@ const ASK_PROMPT_TEMPLATE: &str = include_str!("ask_prompt.txt");
1212
const CODING_PROMPT_TEMPLATE: &str = include_str!("coding_prompt.txt");
1313
const 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
}

crates/agent/src/tool/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ mod tests {
292292
use std::path::PathBuf;
293293
let wd = PathBuf::from(".");
294294
for mode in [ToolMode::Ask, ToolMode::Coding, ToolMode::Plan] {
295-
let sys_blocks = build_system_prompt(mode, &wd, Some("main"), None, None, None);
295+
let sys_blocks = build_system_prompt(mode, &wd, Some("main"), None, None, None, false);
296296
let sys: String = sys_blocks.iter().map(|b| b.text.as_str()).collect::<Vec<_>>().join("\n");
297297
let reg = ToolRegistry::for_mode(mode, None, None);
298298
let tools = reg.tool_definitions();

crates/agent/tests/integration.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,7 @@ async fn test_caching_emits_cache_tokens_across_turns() {
982982
None, // no project note
983983
None, // no skills
984984
None, // no subagents
985+
cfg.context_engine.is_some(),
985986
));
986987

987988
std::fs::write(tmp.path().join("sample.txt"), "hello world\n").unwrap();

0 commit comments

Comments
 (0)