feat(agent-core): strengthen Edit-over-Write preference in tool prompts (#540)

This commit is contained in:
Luyu Cheng 2026-06-10 12:22:25 +08:00 committed by GitHub
parent a1b419ab59
commit 2ebe38769f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 48 additions and 24 deletions

View 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.

View file

@ -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.

View file

@ -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.

View 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',

View file

@ -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 () => {