mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
feat(agent-core): strengthen Edit-over-Write preference in tool prompts (#540)
This commit is contained in:
parent
a1b419ab59
commit
2ebe38769f
5 changed files with 48 additions and 24 deletions
6
.changeset/tighten-file-tool-guidance.md
Normal file
6
.changeset/tighten-file-tool-guidance.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"@moonshot-ai/agent-core": patch
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Tighten file tool guidance to route incremental edits through Edit.
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
Perform exact string replacements against the text view returned by Read.
|
||||
Perform exact replacements in existing files.
|
||||
|
||||
- When copying from Read output, omit the line-number prefix and tab; match only the file content.
|
||||
- By default, old_string must occur exactly once. If it matches multiple locations, add surrounding context or set replace_all when every occurrence should change.
|
||||
- Prefer Edit for targeted changes to existing files; use Write only for new files or complete overwrites.
|
||||
- To modify a file, always use Edit; do not run a Shell `sed` command for edits.
|
||||
- When making several independent changes, issue multiple Edit calls in parallel within a single response; edits to the same file are serialized automatically by a write lock.
|
||||
- When several parallel Edit calls target the same file, a write lock serializes them; they apply in the order the calls appear in your response. An edit fails with `old_string not found` if its old_string was taken from text an earlier edit already replaced — base every old_string on the latest Read view and order dependent edits accordingly.
|
||||
- For pure CRLF files, Read shows LF and Edit.old_string/new_string should use LF; Edit writes the file back with CRLF preserved.
|
||||
- For mixed line endings or lone carriage returns, Read displays carriage returns as \r; include actual \r escapes in old_string/new_string for those positions.
|
||||
- Edit is mandatory for every incremental change, especially small edits. DO NOT use Write or Bash `sed`.
|
||||
- Read the target file before every Edit. DO NOT call Edit from memory, stale context, or a guessed `old_string`.
|
||||
- Take `old_string` and `new_string` from the Read output view.
|
||||
- Drop the line-number prefix and tab; match only file content.
|
||||
- `old_string` must be unique unless `replace_all` is set.
|
||||
- If `old_string` is ambiguous, add surrounding context. Use `replace_all` only when every occurrence should change.
|
||||
- Multiple Edit calls may run in one response only when they do not target the same file.
|
||||
- DO NOT issue consecutive Edit calls on the same file. A previous Edit can invalidate a later Edit's `old_string`, causing `old_string not found`. Read the file again before the next Edit.
|
||||
- A write lock serializes same-file edits in response order, but serialization does not make stale `old_string` valid.
|
||||
- For pure CRLF files, Read shows LF; use LF in `old_string` and `new_string`, and Edit writes CRLF back.
|
||||
- For mixed endings or lone carriage returns, Read shows carriage returns as \r; include actual \r escapes in those positions.
|
||||
|
|
|
|||
|
|
@ -1 +1,10 @@
|
|||
Overwrite or append to a file with content exactly as provided, creating the file if needed; the parent directory must already exist. Defaults to overwrite; append adds content to the end without adding a newline. Write does not use the Read/Edit model text view and does not preserve or infer the previous line-ending style: \n stays LF, \r\n stays CRLF. Use Edit for targeted changes to existing files. When the content is very large, you can split it across multiple calls: write the first chunk with overwrite, then add the remaining chunks with append.
|
||||
Create, append to, or replace a file entirely.
|
||||
|
||||
- The parent directory must already exist.
|
||||
- Mode defaults to overwrite; append adds content at EOF without adding a newline.
|
||||
- Write is NOT ALLOWED for incremental changes to existing files, including trivial, one-line, quick, or cosmetic edits. Use Edit instead.
|
||||
- Use Write only when the file does not exist, you intend a complete replacement, or the new contents have little continuity with the old contents.
|
||||
- Read before overwriting an existing file.
|
||||
- Write ignores the Read/Edit line-number view. NEVER include line prefixes.
|
||||
- Write outputs content literally, including supplied line endings: \n stays LF, \r\n stays CRLF.
|
||||
- For new content too large for one call, overwrite the first chunk, then append subsequent chunks. Never chunk Write to modify an existing file.
|
||||
|
|
|
|||
|
|
@ -34,15 +34,19 @@ describe('EditTool', () => {
|
|||
const tool = new EditTool(createFakeKaos(), PERMISSIVE_WORKSPACE);
|
||||
|
||||
expect(tool.name).toBe('Edit');
|
||||
expect(tool.description).toContain('text view returned by Read');
|
||||
expect(tool.description).toContain('omit the line-number prefix');
|
||||
expect(tool.description).toContain('old_string must occur exactly once');
|
||||
expect(tool.description).toContain('multiple Edit calls in parallel');
|
||||
// Editing files should go through Edit, not a Shell `sed` command.
|
||||
expect(tool.description).toContain('Shell `sed`');
|
||||
expect(tool.description).toContain('Read the target file before every Edit');
|
||||
expect(tool.description).toContain('DO NOT call Edit from memory');
|
||||
expect(tool.description).toContain('Read output view');
|
||||
expect(tool.description).toContain('line-number prefix');
|
||||
expect(tool.description).toContain('`old_string` must be unique');
|
||||
expect(tool.description).toContain('only when they do not target the same file');
|
||||
expect(tool.description).toContain('DO NOT issue consecutive Edit calls on the same file');
|
||||
// Editing files should go through Edit, not Write and not a Bash `sed`
|
||||
// command. The prompt names both alternatives explicitly.
|
||||
expect(tool.description).toContain('DO NOT use Write or Bash `sed`');
|
||||
// Parallel Edit calls on the same file are serialized and applied in
|
||||
// response order; mismatched old_string fails explicitly.
|
||||
expect(tool.description).toContain('they apply in the order the calls appear in your response');
|
||||
expect(tool.description).toContain('same-file edits in response order');
|
||||
expect(tool.description).toContain('old_string not found');
|
||||
expect(tool.parameters).toMatchObject({
|
||||
type: 'object',
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@ describe('WriteTool', () => {
|
|||
const tool = new WriteTool(createFakeKaos(), PERMISSIVE_WORKSPACE);
|
||||
|
||||
expect(tool.name).toBe('Write');
|
||||
expect(tool.description).toContain('exactly as provided');
|
||||
expect(tool.description).toContain('append adds content to the end without adding a newline');
|
||||
expect(tool.description).toContain('does not preserve or infer the previous line-ending style');
|
||||
expect(tool.description).toContain('append adds content at EOF without adding a newline');
|
||||
expect(tool.description).toContain('\\n stays LF, \\r\\n stays CRLF');
|
||||
// The prompt steers the agent toward Edit for partial changes to an
|
||||
// existing file. Pin the prohibition so accidental weakening is caught.
|
||||
expect(tool.description).toContain('Write is NOT ALLOWED for incremental changes');
|
||||
expect(tool.parameters).toMatchObject({
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
|
@ -93,11 +95,11 @@ describe('WriteTool', () => {
|
|||
it('guides batching large content across multiple write calls', () => {
|
||||
const tool = new WriteTool(createFakeKaos(), PERMISSIVE_WORKSPACE);
|
||||
|
||||
// The guidance must mention splitting large content across multiple calls,
|
||||
// and spell out the first-overwrite-then-append ordering.
|
||||
// The guidance must mention that a file too large for one call should be
|
||||
// chunked, and spell out the first-overwrite-then-append ordering.
|
||||
expect(tool.description).toMatch(/large/i);
|
||||
expect(tool.description).toMatch(/split[^.]*multiple calls/i);
|
||||
expect(tool.description).toMatch(/first[^.]*overwrite[^.]*then[^.]*append/i);
|
||||
expect(tool.description).toContain('content too large for one call');
|
||||
expect(tool.description).toMatch(/overwrite[^.]*first chunk[^.]*then[^.]*append/i);
|
||||
});
|
||||
|
||||
it('writes content through kaos and reports bytes written', async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue