Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/tighten-file-tool-guidance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/kimi-code": patch
---

Tighten file tool guidance to route incremental edits through Edit.
21 changes: 12 additions & 9 deletions packages/agent-core/src/tools/builtin/file/edit.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 10 additions & 1 deletion packages/agent-core/src/tools/builtin/file/write.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 11 additions & 7 deletions packages/agent-core/test/tools/edit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
16 changes: 9 additions & 7 deletions packages/agent-core/test/tools/write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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 () => {
Expand Down
Loading