From f2eb8bae981a267aad1afc325515bfdefa0af109 Mon Sep 17 00:00:00 2001 From: chengluyu <2239547+chengluyu@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:04:13 +0800 Subject: [PATCH] feat(agent-core): strengthen Edit-over-Write preference in tool prompts --- .changeset/tighten-file-tool-guidance.md | 6 ++++++ .../agent-core/src/tools/builtin/file/edit.md | 21 +++++++++++-------- .../src/tools/builtin/file/write.md | 11 +++++++++- packages/agent-core/test/tools/edit.test.ts | 18 +++++++++------- packages/agent-core/test/tools/write.test.ts | 16 +++++++------- 5 files changed, 48 insertions(+), 24 deletions(-) create mode 100644 .changeset/tighten-file-tool-guidance.md diff --git a/.changeset/tighten-file-tool-guidance.md b/.changeset/tighten-file-tool-guidance.md new file mode 100644 index 000000000..6ea5b6d3d --- /dev/null +++ b/.changeset/tighten-file-tool-guidance.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/agent-core": patch +"@moonshot-ai/kimi-code": patch +--- + +Tighten file tool guidance to route incremental edits through Edit. diff --git a/packages/agent-core/src/tools/builtin/file/edit.md b/packages/agent-core/src/tools/builtin/file/edit.md index f5de2c551..3123f33fa 100644 --- a/packages/agent-core/src/tools/builtin/file/edit.md +++ b/packages/agent-core/src/tools/builtin/file/edit.md @@ -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. \ No newline at end of file +- 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. diff --git a/packages/agent-core/src/tools/builtin/file/write.md b/packages/agent-core/src/tools/builtin/file/write.md index 5d82cd148..360805e5c 100644 --- a/packages/agent-core/src/tools/builtin/file/write.md +++ b/packages/agent-core/src/tools/builtin/file/write.md @@ -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. diff --git a/packages/agent-core/test/tools/edit.test.ts b/packages/agent-core/test/tools/edit.test.ts index 315049a3f..6e0ad2010 100644 --- a/packages/agent-core/test/tools/edit.test.ts +++ b/packages/agent-core/test/tools/edit.test.ts @@ -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', diff --git a/packages/agent-core/test/tools/write.test.ts b/packages/agent-core/test/tools/write.test.ts index e7d48209d..2c52f1959 100644 --- a/packages/agent-core/test/tools/write.test.ts +++ b/packages/agent-core/test/tools/write.test.ts @@ -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 () => {