Skip to content

Commit 5e03f21

Browse files
authored
feat(prompts): align repository tool contracts (#50)
Inject a shared repository-tool contract into every built-in agent prompt, document canonical context-tool parameters and continuation behavior, and keep the guidance synchronized with registered schemas through regression tests.
1 parent ea6c11e commit 5e03f21

5 files changed

Lines changed: 130 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Added an opt-in real-LLM integration suite for batch-read continuation,
1313
every grep output mode, guarded edit previews and writes, and stable glob
1414
pagination.
15+
- Added a shared repository-tool contract to every built-in agent prompt with
16+
canonical `read`, `grep`, `glob`, and `edit` arguments, pagination rules, and
17+
guarded mechanical-edit guidance.
1518

1619
### Fixed
1720

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
## Repository Tool Contract
2+
3+
The registered tool schema is authoritative for availability, types, and limits.
4+
Use its canonical argument names exactly; do not invent aliases, cursors, or
5+
continuations.
6+
7+
- `read`: use `file_path` with optional 0-based `offset` and `limit` for one
8+
file. When several known text files are relevant, prefer one call with
9+
`files=[{path, offset?, limit?}]` and optional `max_output_bytes`; never send
10+
`file_path` and `files` together. If `metadata.batch.continuation` is non-empty,
11+
copy that exact array into the next call's `files` and stop when it is empty.
12+
- `grep`: pass `pattern` and, when useful, `path`, `glob`, `context`, and `-i`.
13+
Choose the smallest useful `output_mode`: `content` for matching evidence,
14+
`files_with_matches` for paths, `count` for matching-line counts per file, or
15+
`summary` for totals only. Only `files_with_matches` and `count` accept
16+
pagination `limit`/`cursor`; copy `metadata.page.next_cursor` exactly and stop
17+
when it is absent.
18+
- `glob`: pass `pattern` and optional `path`, `limit`, `cursor`, and `sort`.
19+
Keep the default `sort: "backend"` when backend relevance or recency matters;
20+
use `sort: "path"` for deterministic lexical pagination. Copy
21+
`metadata.page.next_cursor` exactly and stop when it is absent.
22+
- `edit`: pass `file_path`, `old_string`, and `new_string`; set `replace_all`
23+
only when every occurrence should change. For `replace_all` or any mechanical
24+
change whose scope is uncertain, first use `dry_run`, inspect the diff and
25+
replacement count, then apply with that count as `expected_replacements` and
26+
an appropriate `max_replacements`. On a count mismatch or version conflict,
27+
re-read and re-preview instead of weakening the guards.
28+
29+
Use dedicated repository tools instead of shell commands for reading, searching,
30+
and editing. Use `bash` for builds, tests, and commands that genuinely require a
31+
shell.

core/src/prompts.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ pub const CONTINUATION: &str = include_str!("../prompts/common/continuation.md")
3535
/// agent styles and delegated subagents (which build through the same path).
3636
pub const BOUNDARIES: &str = include_str!("../prompts/common/boundaries.md");
3737

38+
/// Canonical repository-tool arguments and context-efficient usage strategy.
39+
///
40+
/// Appended to every assembled system prompt so general, planning, exploration,
41+
/// review, and verification styles share one tool contract.
42+
pub const REPOSITORY_TOOL_CONTRACT: &str =
43+
include_str!("../prompts/common/repository_tool_contract.md");
44+
3845
// ============================================================================
3946
// Delegated Run Prompts
4047
// ============================================================================
@@ -446,7 +453,8 @@ impl SystemPromptSlots {
446453
/// Build the final system prompt by assembling slots around the core prompt.
447454
///
448455
/// The core agentic behavior (Core Behaviour, Tool Usage Strategy, Completion
449-
/// Criteria) is always preserved. Users can only customize the edges.
456+
/// Criteria), shared repository-tool contract, and safety boundaries are always
457+
/// preserved. Users can only customize the edges.
450458
///
451459
/// Note: This uses `AgentStyle::GeneralPurpose` as the base. Use
452460
/// `build_with_message()` to enable automatic intent-based style detection.
@@ -504,7 +512,16 @@ impl SystemPromptSlots {
504512

505513
parts.push(core);
506514

507-
// 2b. Safety boundaries — single source of truth, appended uniformly so
515+
// 2b. Repository-tool contract — shared by every style so parameter and
516+
// pagination guidance cannot drift between built-in agents.
517+
parts.push(
518+
REPOSITORY_TOOL_CONTRACT
519+
.replace('\r', "")
520+
.trim_end()
521+
.to_string(),
522+
);
523+
524+
// 2c. Safety boundaries — single source of truth, appended uniformly so
508525
// every style and delegated subagent (which build through this path)
509526
// carries injection-hygiene, secret-handling, and malware-refusal rules.
510527
parts.push(BOUNDARIES.replace('\r', "").trim_end().to_string());

core/src/prompts/tests.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ fn test_all_prompts_loaded() {
66
assert!(!SYSTEM_DEFAULT.is_empty());
77
assert!(!CONTINUATION.is_empty());
88
assert!(!BOUNDARIES.is_empty());
9+
assert!(!REPOSITORY_TOOL_CONTRACT.is_empty());
910
assert!(!AGENT_EXPLORE.is_empty());
1011
assert!(!AGENT_PLAN.is_empty());
1112
assert!(!AGENT_CODE_REVIEW.is_empty());
@@ -100,6 +101,65 @@ fn test_boundaries_not_duplicated_in_general_purpose() {
100101
assert_eq!(built.matches("## Boundaries").count(), 1);
101102
}
102103

104+
#[test]
105+
fn test_repository_tool_contract_is_injected_for_every_style() {
106+
let required_contract = [
107+
"## Repository Tool Contract",
108+
"`file_path`",
109+
"`files`",
110+
"`max_output_bytes`",
111+
"`metadata.batch.continuation`",
112+
"`output_mode`",
113+
"`files_with_matches`",
114+
"`metadata.page.next_cursor`",
115+
"`sort: \"path\"`",
116+
"`dry_run`",
117+
"`expected_replacements`",
118+
"`max_replacements`",
119+
];
120+
121+
for style in [
122+
AgentStyle::GeneralPurpose,
123+
AgentStyle::Plan,
124+
AgentStyle::Verification,
125+
AgentStyle::Explore,
126+
AgentStyle::CodeReview,
127+
] {
128+
let built = SystemPromptSlots::default().with_style(style).build();
129+
for expected in required_contract {
130+
assert!(
131+
built.contains(expected),
132+
"style {style:?} missing repository-tool guidance: {expected}"
133+
);
134+
}
135+
}
136+
}
137+
138+
#[test]
139+
fn test_repository_tool_contract_is_not_duplicated() {
140+
let built = SystemPromptSlots::default().build();
141+
assert_eq!(built.matches("## Repository Tool Contract").count(), 1);
142+
}
143+
144+
#[test]
145+
fn test_repository_tool_contract_tracks_registered_schema_parameters() {
146+
for (tool, schema) in crate::tools::builtin::repository_tool_parameter_schemas() {
147+
assert!(
148+
REPOSITORY_TOOL_CONTRACT.contains(&format!("`{tool}`")),
149+
"repository-tool contract missing tool: {tool}"
150+
);
151+
let properties = schema["properties"]
152+
.as_object()
153+
.unwrap_or_else(|| panic!("{tool} schema has no object properties"));
154+
for parameter in properties.keys() {
155+
assert!(
156+
REPOSITORY_TOOL_CONTRACT.contains(&format!("`{parameter}`")),
157+
"repository-tool contract missing {tool} parameter: {parameter}"
158+
);
159+
}
160+
}
161+
}
162+
103163
#[test]
104164
fn test_slots_custom_role_replaces_default() {
105165
let slots = SystemPromptSlots {
@@ -399,6 +459,7 @@ fn test_code_review_guidelines_appended() {
399459
fn test_prompts_do_not_reference_removed_surfaces() {
400460
let prompts = [
401461
SYSTEM_DEFAULT,
462+
REPOSITORY_TOOL_CONTRACT,
402463
AGENT_VERIFICATION,
403464
PRE_ANALYSIS_SYSTEM,
404465
AGENT_EXPLORE,

core/src/tools/builtin/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,22 @@ pub fn register_builtins(
7777
registry.register_builtin(Arc::new(web_search::WebSearchTool::new()));
7878
}
7979

80+
#[cfg(test)]
81+
pub(crate) fn repository_tool_parameter_schemas() -> Vec<(String, serde_json::Value)> {
82+
use crate::tools::Tool;
83+
84+
let read = read::ReadTool;
85+
let grep = grep::GrepTool;
86+
let glob = glob_tool::GlobTool;
87+
let edit = edit::EditTool;
88+
vec![
89+
(read.name().to_string(), read.parameters()),
90+
(grep.name().to_string(), grep.parameters()),
91+
(glob.name().to_string(), glob.parameters()),
92+
(edit.name().to_string(), edit.parameters()),
93+
]
94+
}
95+
8096
/// Register the batch tool. Must be called after the registry is wrapped in Arc.
8197
pub fn register_batch(registry: &Arc<ToolRegistry>) {
8298
registry.register_builtin(Arc::new(batch::BatchTool::new(Arc::clone(registry))));

0 commit comments

Comments
 (0)