From 39fc83dcf9f44712579fa303fb4d229a9421973e Mon Sep 17 00:00:00 2001 From: Dwin Gharibi Date: Thu, 6 Aug 2026 17:32:40 +0330 Subject: [PATCH 1/6] fix(pkg/tools/builtin/filesystem/filesystem.go): fixing problem of only single first match for edit file in filesystem --- pkg/tools/builtin/filesystem/filesystem.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/tools/builtin/filesystem/filesystem.go b/pkg/tools/builtin/filesystem/filesystem.go index 5b8d1856d..7eb8f2a01 100644 --- a/pkg/tools/builtin/filesystem/filesystem.go +++ b/pkg/tools/builtin/filesystem/filesystem.go @@ -1017,8 +1017,17 @@ func (t *ToolSet) handleEditFile(ctx context.Context, args EditFileArgs) (*tools var changes []string for i, edit := range args.Edits { - if !strings.Contains(modifiedContent, edit.OldText) { + // Counted against the running content, not the original: an earlier edit + // may legitimately have removed a duplicate. Replacing an ambiguous match + // would silently pick the first occurrence, which the caller cannot tell + // apart from the site they meant. + switch n := strings.Count(modifiedContent, edit.OldText); { + case n == 0: return tools.ResultError(fmt.Sprintf("Edit %d failed: old text not found", i+1)), nil + case n > 1: + return tools.ResultError(fmt.Sprintf( + "Edit %d failed: old text appears %d times; include more surrounding context so it matches exactly once", + i+1, n)), nil } modifiedContent = strings.Replace(modifiedContent, edit.OldText, edit.NewText, 1) changes = append(changes, fmt.Sprintf("Edit %d: Replaced %d characters", i+1, len(edit.OldText))) From c3742a9054577a5b69d38cc0921bad7d722234d4 Mon Sep 17 00:00:00 2001 From: Dwin Gharibi Date: Thu, 6 Aug 2026 17:33:08 +0330 Subject: [PATCH 2/6] test(pkg/tools/builtin/filesystem/filesystem_test.go): adding edge case tests for single first match edit bug --- .../builtin/filesystem/filesystem_test.go | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/pkg/tools/builtin/filesystem/filesystem_test.go b/pkg/tools/builtin/filesystem/filesystem_test.go index c09e76ef1..97f3c8718 100644 --- a/pkg/tools/builtin/filesystem/filesystem_test.go +++ b/pkg/tools/builtin/filesystem/filesystem_test.go @@ -532,6 +532,103 @@ func TestFilesystemTool_EditFile(t *testing.T) { assert.Contains(t, result.Output, "old text not found") } +// An oldText that matches more than once is ambiguous: strings.Replace(..., 1) +// would rewrite the first occurrence and report a plain success, so the model +// cannot tell whether it edited the site it meant. The caller has to +// disambiguate with more surrounding context instead. +func TestFilesystemTool_EditFileRejectsAmbiguousMatch(t *testing.T) { + t.Parallel() + + // The same assignment in two different blocks — a realistic shape. + const original = "def dev():\n debug = True\n\ndef prod():\n debug = True\n" + + t.Run("two occurrences are refused", func(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + tool := New(tmpDir) + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "conf.py"), []byte(original), 0o644)) + + result, err := tool.handleEditFile(t.Context(), EditFileArgs{ + Path: "conf.py", + Edits: []Edit{{OldText: " debug = True", NewText: " debug = False"}}, + }) + require.NoError(t, err) + assert.True(t, result.IsError) + assert.Contains(t, result.Output, "appears 2 times") + + after, err := os.ReadFile(filepath.Join(tmpDir, "conf.py")) + require.NoError(t, err) + assert.Equal(t, original, string(after), "an ambiguous edit must not modify the file") + }) + + t.Run("a uniquely matching edit still applies", func(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + tool := New(tmpDir) + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "conf.py"), []byte(original), 0o644)) + + // Enough surrounding context to match exactly once. + result, err := tool.handleEditFile(t.Context(), EditFileArgs{ + Path: "conf.py", + Edits: []Edit{{OldText: "def prod():\n debug = True", NewText: "def prod():\n debug = False"}}, + }) + require.NoError(t, err) + assert.False(t, result.IsError) + + after, err := os.ReadFile(filepath.Join(tmpDir, "conf.py")) + require.NoError(t, err) + assert.Equal(t, "def dev():\n debug = True\n\ndef prod():\n debug = False\n", string(after)) + }) + + // Occurrences must be counted against the running content, not the original: + // an earlier edit can legitimately remove a duplicate and leave the later + // edit unambiguous. + t.Run("an earlier edit may resolve a later edit's ambiguity", func(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + tool := New(tmpDir) + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "conf.py"), []byte(original), 0o644)) + + result, err := tool.handleEditFile(t.Context(), EditFileArgs{ + Path: "conf.py", + Edits: []Edit{ + // Removes the first duplicate, using surrounding context. + {OldText: "def dev():\n debug = True", NewText: "def dev():\n debug = None"}, + // Now matches exactly once. + {OldText: " debug = True", NewText: " debug = False"}, + }, + }) + require.NoError(t, err) + assert.False(t, result.IsError, "got: %s", result.Output) + + after, err := os.ReadFile(filepath.Join(tmpDir, "conf.py")) + require.NoError(t, err) + assert.Equal(t, "def dev():\n debug = None\n\ndef prod():\n debug = False\n", string(after)) + }) + + t.Run("an ambiguous later edit discards the earlier one", func(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + tool := New(tmpDir) + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "conf.py"), []byte(original), 0o644)) + + result, err := tool.handleEditFile(t.Context(), EditFileArgs{ + Path: "conf.py", + Edits: []Edit{ + {OldText: "def dev():", NewText: "def development():"}, + {OldText: " debug = True", NewText: " debug = False"}, + }, + }) + require.NoError(t, err) + assert.True(t, result.IsError) + assert.Contains(t, result.Output, "Edit 2") + + after, err := os.ReadFile(filepath.Join(tmpDir, "conf.py")) + require.NoError(t, err) + assert.Equal(t, original, string(after), "no edit may be persisted when a later one is rejected") + }) +} + func TestParseEditFileArgs(t *testing.T) { t.Parallel() From e1926afb800158436b398bf228e1735d13ebd519 Mon Sep 17 00:00:00 2001 From: Dwin Gharibi Date: Fri, 7 Aug 2026 11:08:38 +0330 Subject: [PATCH 3/6] refactor(filesystem): share the edit validity rule with the ACP toolset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ACP toolset overrides edit_file with its own client-backed handler while serving the same tool name and schema, but carried a copy of the pre-fix loop — so an ambiguous edit was refused over the built-in transport and silently applied to the first match over ACP. The same tool call meant two different things. Extracts EditFailureReason as the single rule both loops apply, so these semantics cannot drift apart again. The rule also gains an explicit empty-oldText branch, checked before the occurrence count: strings.Count(s, "") returns the rune count plus one, so an empty oldText previously fell into the n > 1 arm and reported a meaningless "appears 19 times" with advice that could not be satisfied. The ambiguous-match message now names both remedies. Repeating an identical edit used to be how this schema expressed "change every occurrence", and refusing it is a deliberate behaviour change — but "include more surrounding context" alone reads as "your text is wrong" to a model whose intent was every site, sending it into a useless retry. --- pkg/acp/filesystem.go | 7 +++- pkg/tools/builtin/filesystem/filesystem.go | 48 +++++++++++++++++----- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/pkg/acp/filesystem.go b/pkg/acp/filesystem.go index de9878427..f6d9158f1 100644 --- a/pkg/acp/filesystem.go +++ b/pkg/acp/filesystem.go @@ -282,8 +282,11 @@ func (t *FilesystemToolset) handleEditFile(ctx context.Context, toolCall tools.T modifiedContent := resp.Content for i, edit := range args.Edits { - if !strings.Contains(modifiedContent, edit.OldText) { - return tools.ResultError(fmt.Sprintf("Edit %d failed: old text not found", i+1)), nil + // Shared with the built-in filesystem toolset: this handler overrides the + // same edit_file tool name and schema, so both must agree on what a valid + // edit is or the call means different things depending on transport. + if reason := filesystem.EditFailureReason(modifiedContent, edit); reason != "" { + return tools.ResultError(fmt.Sprintf("Edit %d failed: %s", i+1, reason)), nil } modifiedContent = strings.Replace(modifiedContent, edit.OldText, edit.NewText, 1) } diff --git a/pkg/tools/builtin/filesystem/filesystem.go b/pkg/tools/builtin/filesystem/filesystem.go index 7eb8f2a01..f68b43493 100644 --- a/pkg/tools/builtin/filesystem/filesystem.go +++ b/pkg/tools/builtin/filesystem/filesystem.go @@ -997,6 +997,39 @@ func (t *ToolSet) editFileHandler() tools.ToolHandler { } } +// EditFailureReason reports why edit cannot be applied to content, or an empty +// string when it can be applied to exactly one site. +// +// It is exported because the ACP toolset overrides edit_file with its own +// client-backed handler while serving the same tool name and schema +// (pkg/acp/filesystem.go). Both loops must agree on what a valid edit is, or the +// same tool call means different things depending on transport — so the rule +// lives here once rather than being duplicated per handler. +// +// Callers supply their own "Edit N failed: " prefix. +func EditFailureReason(content string, edit Edit) string { + // strings.Contains always matches "" and strings.Replace would insert + // newText at offset 0, silently prepending to the file. Checked before the + // occurrence count because strings.Count(s, "") returns the rune count plus + // one, which would otherwise report a meaningless "appears 42 times". + if edit.OldText == "" { + return "oldText must not be empty" + } + + switch n := strings.Count(content, edit.OldText); { + case n == 0: + return "old text not found" + case n > 1: + // Naming the count and both remedies matters: the model's intent may + // have been a single site (needs more context) or every site (needs one + // edit per occurrence, which is how this schema expresses replace-all). + // "Your text is wrong" alone would send it into a useless retry. + return fmt.Sprintf("old text appears %d times; include more surrounding context so it "+ + "matches exactly once, or send one edit per occurrence to change several", n) + } + return "" +} + func (t *ToolSet) handleEditFile(ctx context.Context, args EditFileArgs) (*tools.ToolCallResult, error) { annotateFilesystemSpan(ctx, "edit_file", args.Path) resolvedPath, err := t.resolveAndCheckPath(args.Path) @@ -1017,17 +1050,10 @@ func (t *ToolSet) handleEditFile(ctx context.Context, args EditFileArgs) (*tools var changes []string for i, edit := range args.Edits { - // Counted against the running content, not the original: an earlier edit - // may legitimately have removed a duplicate. Replacing an ambiguous match - // would silently pick the first occurrence, which the caller cannot tell - // apart from the site they meant. - switch n := strings.Count(modifiedContent, edit.OldText); { - case n == 0: - return tools.ResultError(fmt.Sprintf("Edit %d failed: old text not found", i+1)), nil - case n > 1: - return tools.ResultError(fmt.Sprintf( - "Edit %d failed: old text appears %d times; include more surrounding context so it matches exactly once", - i+1, n)), nil + // Checked against the running content, not the original: an earlier edit + // may legitimately have removed a duplicate. + if reason := EditFailureReason(modifiedContent, edit); reason != "" { + return tools.ResultError(fmt.Sprintf("Edit %d failed: %s", i+1, reason)), nil } modifiedContent = strings.Replace(modifiedContent, edit.OldText, edit.NewText, 1) changes = append(changes, fmt.Sprintf("Edit %d: Replaced %d characters", i+1, len(edit.OldText))) From 661616f63c8ea32690e61efe37d3261fb596b12b Mon Sep 17 00:00:00 2001 From: Dwin Gharibi Date: Fri, 7 Aug 2026 11:08:38 +0330 Subject: [PATCH 4/6] test(filesystem): cover the shared edit rule and replace-all intent Tests EditFailureReason directly since it is now the rule both the built-in and ACP handlers apply, including that an empty oldText reports its own message rather than an occurrence count. Also pins that multi-occurrence intent stays expressible: the old spelling (the same edit twice) is refused, and one context-extended edit per occurrence achieves what the model meant. --- .../builtin/filesystem/filesystem_test.go | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/pkg/tools/builtin/filesystem/filesystem_test.go b/pkg/tools/builtin/filesystem/filesystem_test.go index 97f3c8718..056042f41 100644 --- a/pkg/tools/builtin/filesystem/filesystem_test.go +++ b/pkg/tools/builtin/filesystem/filesystem_test.go @@ -1633,3 +1633,86 @@ func TestFilesystemTool_RootedListDirRefusesSymlinkSwap(t *testing.T) { require.Error(t, err, "rooted readDir must refuse a directory symlink that escapes the allow-list") } + +// EditFailureReason is the single rule both the built-in handler and the ACP +// override apply, so it is tested directly rather than only through them. +func TestEditFailureReason(t *testing.T) { + t.Parallel() + + const content = "a = 1\nb = 2\na = 1\n" + + t.Run("applicable edit has no reason", func(t *testing.T) { + t.Parallel() + assert.Empty(t, EditFailureReason(content, Edit{OldText: "b = 2", NewText: "b = 9"})) + }) + + t.Run("missing text", func(t *testing.T) { + t.Parallel() + assert.Equal(t, "old text not found", + EditFailureReason(content, Edit{OldText: "nope", NewText: "x"})) + }) + + // strings.Count(s, "") returns the rune count plus one, so an empty oldText + // would otherwise fall into the n > 1 arm and report a meaningless + // "appears 19 times" with advice that cannot be satisfied. + t.Run("empty oldText gets its own message, not an occurrence count", func(t *testing.T) { + t.Parallel() + reason := EditFailureReason(content, Edit{OldText: "", NewText: "x"}) + assert.Equal(t, "oldText must not be empty", reason) + assert.NotContains(t, reason, "appears") + }) + + t.Run("ambiguous match names the count and both remedies", func(t *testing.T) { + t.Parallel() + reason := EditFailureReason(content, Edit{OldText: "a = 1", NewText: "a = 9"}) + assert.Contains(t, reason, "appears 2 times") + assert.Contains(t, reason, "more surrounding context") + // A model whose intent was *every* occurrence needs to be told how this + // schema expresses that, or it retries the same payload. + assert.Contains(t, reason, "one edit per occurrence") + }) +} + +// Repeating an identical edit used to be the way to express "change every +// occurrence", and refusing it is a deliberate behaviour change. The intent must +// still be expressible, which is what the error message now points at. +func TestFilesystemTool_EditFileMultiOccurrenceIntentRemainsExpressible(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + tool := New(tmpDir) + const original = "a = 1\nb = 2\na = 1\n" + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "conf.py"), []byte(original), 0o644)) + + // The old spelling — the same edit twice — is now refused. + result, err := tool.handleEditFile(t.Context(), EditFileArgs{ + Path: "conf.py", + Edits: []Edit{ + {OldText: "a = 1", NewText: "a = 9"}, + {OldText: "a = 1", NewText: "a = 9"}, + }, + }) + require.NoError(t, err) + require.True(t, result.IsError) + assert.Contains(t, result.Output, "one edit per occurrence") + + after, err := os.ReadFile(filepath.Join(tmpDir, "conf.py")) + require.NoError(t, err) + require.Equal(t, original, string(after), "the refused batch must not have written") + + // One edit per occurrence, each carrying enough context to be unique, does + // what the model meant. + result, err = tool.handleEditFile(t.Context(), EditFileArgs{ + Path: "conf.py", + Edits: []Edit{ + {OldText: "a = 1\nb = 2", NewText: "a = 9\nb = 2"}, + {OldText: "b = 2\na = 1", NewText: "b = 2\na = 9"}, + }, + }) + require.NoError(t, err) + require.False(t, result.IsError, result.Output) + + after, err = os.ReadFile(filepath.Join(tmpDir, "conf.py")) + require.NoError(t, err) + assert.Equal(t, "a = 9\nb = 2\na = 9\n", string(after)) +} From b945b489afdfacbf4426720db57c0cde9df8dc59 Mon Sep 17 00:00:00 2001 From: Dwin Gharibi Date: Wed, 19 Aug 2026 10:10:35 +0330 Subject: [PATCH 5/6] docs(filesystem): state the edit_file uniqueness rule in the tool contract The rule was enforced but never advertised, so every ambiguous or empty oldText cost a full round trip the model could not have avoided. The schema description, the tool description and the toolset docs now all say oldText must be non-empty and match exactly once, and how to satisfy that. The five e2e cassettes are matched on the whole normalized request body and embed the schema, so they carry the same text; reverting one of them fails the test with "requested interaction not found", which is the check that the request bodies and the code still agree. --- docs/tools/filesystem/index.md | 2 +- e2e/testdata/cassettes/TestExec_Anthropic_ToolCall.yaml | 4 ++-- e2e/testdata/cassettes/TestExec_Gemini_ToolCall.yaml | 4 ++-- e2e/testdata/cassettes/TestExec_Mistral_ToolCall.yaml | 4 ++-- e2e/testdata/cassettes/TestExec_OpenAI_HideToolCalls.yaml | 4 ++-- e2e/testdata/cassettes/TestExec_OpenAI_ToolCall.yaml | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/tools/filesystem/index.md b/docs/tools/filesystem/index.md index 19b0be53e..a469a1a5e 100644 --- a/docs/tools/filesystem/index.md +++ b/docs/tools/filesystem/index.md @@ -44,7 +44,7 @@ This helps agents distinguish between an empty directory and a tool failure, avo | `read_file` | Read the contents of a file (whole file, or a line range of a text file) | | `read_multiple_files` | Read several files in one call (more efficient than multiple `read_file`) | | `write_file` | Create or overwrite a file with new content | -| `edit_file` | Make line-based edits (find-and-replace) in an existing file. Each edit must specify a non-empty `oldText` to match and replace; empty `oldText` values are rejected with an error. | +| `edit_file` | Make line-based edits (find-and-replace) in an existing file. Each edit must specify a non-empty `oldText` that matches exactly once in the file; empty `oldText` values and ambiguous matches are rejected with an error naming the occurrence count. Add surrounding context to disambiguate, or send one edit per occurrence to change several. | | `list_directory` | List files and directories at a given path (explicitly reports empty directories) | | `directory_tree` | Recursive tree view of a directory | | `create_directory` | Create a new directory (creates parent directories as needed) | diff --git a/e2e/testdata/cassettes/TestExec_Anthropic_ToolCall.yaml b/e2e/testdata/cassettes/TestExec_Anthropic_ToolCall.yaml index b16bf9704..3f07f7a80 100644 --- a/e2e/testdata/cassettes/TestExec_Anthropic_ToolCall.yaml +++ b/e2e/testdata/cassettes/TestExec_Anthropic_ToolCall.yaml @@ -8,7 +8,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.anthropic.com - body: '{"max_tokens":64000,"messages":[{"content":[{"text":"How many files in testdata/working_dir? Only output the number.","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-sonnet-4-0","system":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.","type":"text"},{"text":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[{"input_schema":{"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"},"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure."},{"input_schema":{"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["path","edits"],"type":"object"},"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content."},{"input_schema":{"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"},"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path."},{"input_schema":{"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["path"],"type":"object"},"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly."},{"input_schema":{"properties":{"json":{"description":"Return result as JSON","type":"boolean"},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously."},{"input_schema":{"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":"boolean"},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["path","query"],"type":"object"},"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern."},{"input_schema":{"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["path","content"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content."},{"input_schema":{"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"create_directory","description":"Create one or more new directories or nested directory structures."},{"input_schema":{"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"remove_directory","description":"Remove one or more empty directories."}],"stream":true}' + body: '{"max_tokens":64000,"messages":[{"content":[{"text":"How many files in testdata/working_dir? Only output the number.","cache_control":{"type":"ephemeral"},"type":"text"}],"role":"user"}],"model":"claude-sonnet-4-0","system":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.","type":"text"},{"text":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[{"input_schema":{"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"},"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure."},{"input_schema":{"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace. Must be non-empty and must match exactly once in the file: include surrounding context to disambiguate, or send one edit per occurrence to change several.","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["path","edits"],"type":"object"},"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content. Every oldText must match exactly once."},{"input_schema":{"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"},"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path."},{"input_schema":{"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["path"],"type":"object"},"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly."},{"input_schema":{"properties":{"json":{"description":"Return result as JSON","type":"boolean"},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously."},{"input_schema":{"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":"boolean"},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["path","query"],"type":"object"},"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern."},{"input_schema":{"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["path","content"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content."},{"input_schema":{"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"create_directory","description":"Create one or more new directories or nested directory structures."},{"input_schema":{"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"remove_directory","description":"Remove one or more empty directories."}],"stream":true}' url: https://api.anthropic.com/v1/messages method: POST response: @@ -55,7 +55,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.anthropic.com - body: '{"max_tokens":64000,"messages":[{"content":[{"text":"How many files in testdata/working_dir? Only output the number.","type":"text"}],"role":"user"},{"content":[{"id":"toolu_012gmfqnoTX8c5aV3vMWUnas","input":{"path":"testdata/working_dir"},"name":"list_directory","cache_control":{"type":"ephemeral"},"type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"toolu_012gmfqnoTX8c5aV3vMWUnas","is_error":false,"cache_control":{"type":"ephemeral"},"content":[{"text":"FILE README.me\n","type":"text"}],"type":"tool_result"}],"role":"user"}],"model":"claude-sonnet-4-0","system":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.","type":"text"},{"text":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[{"input_schema":{"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"},"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure."},{"input_schema":{"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["path","edits"],"type":"object"},"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content."},{"input_schema":{"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"},"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path."},{"input_schema":{"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["path"],"type":"object"},"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly."},{"input_schema":{"properties":{"json":{"description":"Return result as JSON","type":"boolean"},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously."},{"input_schema":{"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":"boolean"},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["path","query"],"type":"object"},"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern."},{"input_schema":{"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["path","content"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content."},{"input_schema":{"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"create_directory","description":"Create one or more new directories or nested directory structures."},{"input_schema":{"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"remove_directory","description":"Remove one or more empty directories."}],"stream":true}' + body: '{"max_tokens":64000,"messages":[{"content":[{"text":"How many files in testdata/working_dir? Only output the number.","type":"text"}],"role":"user"},{"content":[{"id":"toolu_012gmfqnoTX8c5aV3vMWUnas","input":{"path":"testdata/working_dir"},"name":"list_directory","cache_control":{"type":"ephemeral"},"type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"toolu_012gmfqnoTX8c5aV3vMWUnas","is_error":false,"cache_control":{"type":"ephemeral"},"content":[{"text":"FILE README.me\n","type":"text"}],"type":"tool_result"}],"role":"user"}],"model":"claude-sonnet-4-0","system":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.","type":"text"},{"text":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","cache_control":{"type":"ephemeral"},"type":"text"}],"tools":[{"input_schema":{"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"},"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure."},{"input_schema":{"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace. Must be non-empty and must match exactly once in the file: include surrounding context to disambiguate, or send one edit per occurrence to change several.","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["path","edits"],"type":"object"},"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content. Every oldText must match exactly once."},{"input_schema":{"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"},"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path."},{"input_schema":{"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["path"],"type":"object"},"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly."},{"input_schema":{"properties":{"json":{"description":"Return result as JSON","type":"boolean"},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously."},{"input_schema":{"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":"boolean"},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["path","query"],"type":"object"},"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern."},{"input_schema":{"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["path","content"],"type":"object"},"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content."},{"input_schema":{"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"create_directory","description":"Create one or more new directories or nested directory structures."},{"input_schema":{"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"},"name":"remove_directory","description":"Remove one or more empty directories."}],"stream":true}' url: https://api.anthropic.com/v1/messages method: POST response: diff --git a/e2e/testdata/cassettes/TestExec_Gemini_ToolCall.yaml b/e2e/testdata/cassettes/TestExec_Gemini_ToolCall.yaml index 6588b8dcb..8307069d1 100644 --- a/e2e/testdata/cassettes/TestExec_Gemini_ToolCall.yaml +++ b/e2e/testdata/cassettes/TestExec_Gemini_ToolCall.yaml @@ -9,7 +9,7 @@ interactions: content_length: 0 host: generativelanguage.googleapis.com body: | - {"contents":[{"parts":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n"}],"role":"user"},{"parts":[{"text":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output"}],"role":"user"},{"parts":[{"text":"How many files in testdata/working_dir? Only output the number."}],"role":"user"}],"generationConfig":{"maxOutputTokens":65536,"thinkingConfig":{"thinkingBudget":0}},"toolConfig":{"functionCallingConfig":{"mode":"AUTO"}},"tools":[{"functionDeclarations":[{"description":"Get a recursive tree view of files and directories as a JSON structure.","name":"directory_tree","parameters":{"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},{"description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","name":"edit_file","parameters":{"properties":{"edits":{"description":"Edits to apply","items":{"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["path","edits"],"type":"object"}},{"description":"Get a detailed listing of all files and directories in a specified path.","name":"list_directory","parameters":{"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},{"description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","name":"read_file","parameters":{"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":"integer"},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":"integer"},"path":{"description":"File to read","type":"string"}},"required":["path"],"type":"object"}},{"description":"Read the contents of multiple files simultaneously.","name":"read_multiple_files","parameters":{"properties":{"json":{"description":"Return result as JSON","type":"boolean"},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},{"description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","name":"search_files_content","parameters":{"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":"array"},"is_regex":{"description":"Treat query as regex","type":"boolean"},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["path","query"],"type":"object"}},{"description":"Create a new file or completely overwrite an existing file with new content.","name":"write_file","parameters":{"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["path","content"],"type":"object"}},{"description":"Create one or more new directories or nested directory structures.","name":"create_directory","parameters":{"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},{"description":"Remove one or more empty directories.","name":"remove_directory","parameters":{"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}}]}]} + {"contents":[{"parts":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n"}],"role":"user"},{"parts":[{"text":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output"}],"role":"user"},{"parts":[{"text":"How many files in testdata/working_dir? Only output the number."}],"role":"user"}],"generationConfig":{"maxOutputTokens":65536,"thinkingConfig":{"thinkingBudget":0}},"toolConfig":{"functionCallingConfig":{"mode":"AUTO"}},"tools":[{"functionDeclarations":[{"description":"Get a recursive tree view of files and directories as a JSON structure.","name":"directory_tree","parameters":{"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},{"description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content. Every oldText must match exactly once.","name":"edit_file","parameters":{"properties":{"edits":{"description":"Edits to apply","items":{"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace. Must be non-empty and must match exactly once in the file: include surrounding context to disambiguate, or send one edit per occurrence to change several.","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["path","edits"],"type":"object"}},{"description":"Get a detailed listing of all files and directories in a specified path.","name":"list_directory","parameters":{"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},{"description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","name":"read_file","parameters":{"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":"integer"},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":"integer"},"path":{"description":"File to read","type":"string"}},"required":["path"],"type":"object"}},{"description":"Read the contents of multiple files simultaneously.","name":"read_multiple_files","parameters":{"properties":{"json":{"description":"Return result as JSON","type":"boolean"},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},{"description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","name":"search_files_content","parameters":{"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":"array"},"is_regex":{"description":"Treat query as regex","type":"boolean"},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["path","query"],"type":"object"}},{"description":"Create a new file or completely overwrite an existing file with new content.","name":"write_file","parameters":{"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["path","content"],"type":"object"}},{"description":"Create one or more new directories or nested directory structures.","name":"create_directory","parameters":{"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},{"description":"Remove one or more empty directories.","name":"remove_directory","parameters":{"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}}]}]} form: alt: - sse @@ -33,7 +33,7 @@ interactions: content_length: 0 host: generativelanguage.googleapis.com body: | - {"contents":[{"parts":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n"}],"role":"user"},{"parts":[{"text":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output"}],"role":"user"},{"parts":[{"text":"How many files in testdata/working_dir? Only output the number."}],"role":"user"},{"parts":[{"functionCall":{"args":{"path":"testdata/working_dir"},"name":"list_directory"},"thoughtSignature":"c2tpcF90aG91Z2h0X3NpZ25hdHVyZV92YWxpZGF0b3I="}],"role":"model"},{"parts":[{"functionResponse":{"name":"call_3df8565b-a1ef-4490-95f9-5d94296d7687","response":{"result":"FILE README.me\n"}}}],"role":"user"}],"generationConfig":{"maxOutputTokens":65536,"thinkingConfig":{"thinkingBudget":0}},"toolConfig":{"functionCallingConfig":{"mode":"AUTO"}},"tools":[{"functionDeclarations":[{"description":"Get a recursive tree view of files and directories as a JSON structure.","name":"directory_tree","parameters":{"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},{"description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","name":"edit_file","parameters":{"properties":{"edits":{"description":"Edits to apply","items":{"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["path","edits"],"type":"object"}},{"description":"Get a detailed listing of all files and directories in a specified path.","name":"list_directory","parameters":{"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},{"description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","name":"read_file","parameters":{"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":"integer"},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":"integer"},"path":{"description":"File to read","type":"string"}},"required":["path"],"type":"object"}},{"description":"Read the contents of multiple files simultaneously.","name":"read_multiple_files","parameters":{"properties":{"json":{"description":"Return result as JSON","type":"boolean"},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},{"description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","name":"search_files_content","parameters":{"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":"array"},"is_regex":{"description":"Treat query as regex","type":"boolean"},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["path","query"],"type":"object"}},{"description":"Create a new file or completely overwrite an existing file with new content.","name":"write_file","parameters":{"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["path","content"],"type":"object"}},{"description":"Create one or more new directories or nested directory structures.","name":"create_directory","parameters":{"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},{"description":"Remove one or more empty directories.","name":"remove_directory","parameters":{"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}}]}]} + {"contents":[{"parts":[{"text":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n"}],"role":"user"},{"parts":[{"text":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output"}],"role":"user"},{"parts":[{"text":"How many files in testdata/working_dir? Only output the number."}],"role":"user"},{"parts":[{"functionCall":{"args":{"path":"testdata/working_dir"},"name":"list_directory"},"thoughtSignature":"c2tpcF90aG91Z2h0X3NpZ25hdHVyZV92YWxpZGF0b3I="}],"role":"model"},{"parts":[{"functionResponse":{"name":"call_3df8565b-a1ef-4490-95f9-5d94296d7687","response":{"result":"FILE README.me\n"}}}],"role":"user"}],"generationConfig":{"maxOutputTokens":65536,"thinkingConfig":{"thinkingBudget":0}},"toolConfig":{"functionCallingConfig":{"mode":"AUTO"}},"tools":[{"functionDeclarations":[{"description":"Get a recursive tree view of files and directories as a JSON structure.","name":"directory_tree","parameters":{"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},{"description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content. Every oldText must match exactly once.","name":"edit_file","parameters":{"properties":{"edits":{"description":"Edits to apply","items":{"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace. Must be non-empty and must match exactly once in the file: include surrounding context to disambiguate, or send one edit per occurrence to change several.","type":"string"}},"required":["oldText","newText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["path","edits"],"type":"object"}},{"description":"Get a detailed listing of all files and directories in a specified path.","name":"list_directory","parameters":{"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},{"description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","name":"read_file","parameters":{"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":"integer"},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":"integer"},"path":{"description":"File to read","type":"string"}},"required":["path"],"type":"object"}},{"description":"Read the contents of multiple files simultaneously.","name":"read_multiple_files","parameters":{"properties":{"json":{"description":"Return result as JSON","type":"boolean"},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},{"description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","name":"search_files_content","parameters":{"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":"array"},"is_regex":{"description":"Treat query as regex","type":"boolean"},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["path","query"],"type":"object"}},{"description":"Create a new file or completely overwrite an existing file with new content.","name":"write_file","parameters":{"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["path","content"],"type":"object"}},{"description":"Create one or more new directories or nested directory structures.","name":"create_directory","parameters":{"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},{"description":"Remove one or more empty directories.","name":"remove_directory","parameters":{"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}}]}]} form: alt: - sse diff --git a/e2e/testdata/cassettes/TestExec_Mistral_ToolCall.yaml b/e2e/testdata/cassettes/TestExec_Mistral_ToolCall.yaml index ab0a21e90..416e98968 100644 --- a/e2e/testdata/cassettes/TestExec_Mistral_ToolCall.yaml +++ b/e2e/testdata/cassettes/TestExec_Mistral_ToolCall.yaml @@ -8,7 +8,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.mistral.ai - body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"}],"model":"mistral-small","max_tokens":32000,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' + body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"}],"model":"mistral-small","max_tokens":32000,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content. Every oldText must match exactly once.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace. Must be non-empty and must match exactly once in the file: include surrounding context to disambiguate, or send one edit per occurrence to change several.","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' url: https://api.mistral.ai/v1/chat/completions method: POST response: @@ -34,7 +34,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.mistral.ai - body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"},{"tool_calls":[{"id":"D9WYdiHxV","function":{"arguments":"{\"path\": \"testdata/working_dir\"}","name":"list_directory"},"type":"function"}],"role":"assistant"},{"content":"FILE README.me\n","tool_call_id":"D9WYdiHxV","role":"tool"}],"model":"mistral-small","max_tokens":32000,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' + body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"},{"tool_calls":[{"id":"D9WYdiHxV","function":{"arguments":"{\"path\": \"testdata/working_dir\"}","name":"list_directory"},"type":"function"}],"role":"assistant"},{"content":"FILE README.me\n","tool_call_id":"D9WYdiHxV","role":"tool"}],"model":"mistral-small","max_tokens":32000,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content. Every oldText must match exactly once.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace. Must be non-empty and must match exactly once in the file: include surrounding context to disambiguate, or send one edit per occurrence to change several.","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' url: https://api.mistral.ai/v1/chat/completions method: POST response: diff --git a/e2e/testdata/cassettes/TestExec_OpenAI_HideToolCalls.yaml b/e2e/testdata/cassettes/TestExec_OpenAI_HideToolCalls.yaml index 7702ab6f1..19efb5a03 100644 --- a/e2e/testdata/cassettes/TestExec_OpenAI_HideToolCalls.yaml +++ b/e2e/testdata/cassettes/TestExec_OpenAI_HideToolCalls.yaml @@ -8,7 +8,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.openai.com - body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"}],"model":"gpt-4o","max_tokens":16384,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' + body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"}],"model":"gpt-4o","max_tokens":16384,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content. Every oldText must match exactly once.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace. Must be non-empty and must match exactly once in the file: include surrounding context to disambiguate, or send one edit per occurrence to change several.","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' url: https://api.openai.com/v1/chat/completions method: POST response: @@ -54,7 +54,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.openai.com - body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"},{"tool_calls":[{"id":"call_dsl9jWekN0H1do1ClfeyR1iA","function":{"arguments":"{\"path\":\"testdata/working_dir\"}","name":"list_directory"},"type":"function"}],"role":"assistant"},{"content":"FILE README.me\n","tool_call_id":"call_dsl9jWekN0H1do1ClfeyR1iA","role":"tool"}],"model":"gpt-4o","max_tokens":16384,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' + body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"},{"tool_calls":[{"id":"call_dsl9jWekN0H1do1ClfeyR1iA","function":{"arguments":"{\"path\":\"testdata/working_dir\"}","name":"list_directory"},"type":"function"}],"role":"assistant"},{"content":"FILE README.me\n","tool_call_id":"call_dsl9jWekN0H1do1ClfeyR1iA","role":"tool"}],"model":"gpt-4o","max_tokens":16384,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content. Every oldText must match exactly once.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace. Must be non-empty and must match exactly once in the file: include surrounding context to disambiguate, or send one edit per occurrence to change several.","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' url: https://api.openai.com/v1/chat/completions method: POST response: diff --git a/e2e/testdata/cassettes/TestExec_OpenAI_ToolCall.yaml b/e2e/testdata/cassettes/TestExec_OpenAI_ToolCall.yaml index 36600c929..f071a38ff 100644 --- a/e2e/testdata/cassettes/TestExec_OpenAI_ToolCall.yaml +++ b/e2e/testdata/cassettes/TestExec_OpenAI_ToolCall.yaml @@ -8,7 +8,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.openai.com - body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"}],"model":"gpt-4o","max_tokens":16384,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' + body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"}],"model":"gpt-4o","max_tokens":16384,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content. Every oldText must match exactly once.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace. Must be non-empty and must match exactly once in the file: include surrounding context to disambiguate, or send one edit per occurrence to change several.","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' url: https://api.openai.com/v1/chat/completions method: POST response: @@ -54,7 +54,7 @@ interactions: proto_minor: 1 content_length: 0 host: api.openai.com - body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"},{"tool_calls":[{"id":"call_I1tmAsYKD7bveEpXORJwVFgs","function":{"arguments":"{\"path\":\"testdata/working_dir\"}","name":"list_directory"},"type":"function"}],"role":"assistant"},{"content":"FILE README.me\n","tool_call_id":"call_I1tmAsYKD7bveEpXORJwVFgs","role":"tool"}],"model":"gpt-4o","max_tokens":16384,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' + body: '{"messages":[{"content":"You are a knowledgeable assistant that helps users with various tasks.\nBe helpful, accurate, and concise in your responses.\n","role":"system"},{"content":"## Filesystem Tools\n\n- The working directory is \"/tmp/wd\"; relative paths resolve from it\n- Absolute paths must match the host OS (e.g. C:\\... on Windows, /... on Unix)\n- Prefer read_multiple_files over sequential read_file calls\n- Use search_files_content to locate code or text across files\n- Use exclude patterns in searches and max_depth in directory_tree to limit output","role":"system"},{"content":"How many files in testdata/working_dir? Only output the number.","role":"user"},{"tool_calls":[{"id":"call_I1tmAsYKD7bveEpXORJwVFgs","function":{"arguments":"{\"path\":\"testdata/working_dir\"}","name":"list_directory"},"type":"function"}],"role":"assistant"},{"content":"FILE README.me\n","tool_call_id":"call_I1tmAsYKD7bveEpXORJwVFgs","role":"tool"}],"model":"gpt-4o","max_tokens":16384,"stream_options":{"include_usage":true},"tools":[{"function":{"name":"directory_tree","description":"Get a recursive tree view of files and directories as a JSON structure.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to traverse","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"edit_file","description":"Make line-based edits to a text file. Each edit replaces exact line sequences with new content. Every oldText must match exactly once.","parameters":{"additionalProperties":false,"properties":{"edits":{"description":"Edits to apply","items":{"additionalProperties":false,"properties":{"newText":{"description":"Replacement text","type":"string"},"oldText":{"description":"Exact text to replace. Must be non-empty and must match exactly once in the file: include surrounding context to disambiguate, or send one edit per occurrence to change several.","type":"string"}},"required":["newText","oldText"],"type":"object"},"type":"array"},"path":{"description":"File to edit","type":"string"}},"required":["edits","path"],"type":"object"}},"type":"function"},{"function":{"name":"list_directory","description":"Get a detailed listing of all files and directories in a specified path.","parameters":{"additionalProperties":false,"properties":{"path":{"description":"Directory to list","type":"string"}},"required":["path"],"type":"object"}},"type":"function"},{"function":{"name":"read_file","description":"Read the contents of a file from the file system. By default the complete file is returned; for text files the optional line (1-based start line) and limit (maximum number of lines) arguments select a line range. Supports text files and images (jpg, png, gif, webp). Images are returned as image content that you can view directly.","parameters":{"additionalProperties":false,"properties":{"limit":{"description":"Maximum number of lines to read (text files only; defaults to reading through the end of the file)","type":["null","integer"]},"line":{"description":"1-based line number to start reading from (text files only; defaults to the first line)","type":["null","integer"]},"path":{"description":"File to read","type":"string"}},"required":["limit","line","path"],"type":"object"}},"type":"function"},{"function":{"name":"read_multiple_files","description":"Read the contents of multiple files simultaneously.","parameters":{"additionalProperties":false,"properties":{"json":{"description":"Return result as JSON","type":["boolean","null"]},"paths":{"description":"Files to read","items":{"type":"string"},"type":"array"}},"required":["json","paths"],"type":"object"}},"type":"function"},{"function":{"name":"search_files_content","description":"Searches for text or regex patterns in the content of files matching a GLOB pattern.","parameters":{"additionalProperties":false,"properties":{"excludePatterns":{"description":"Patterns to exclude","items":{"type":"string"},"type":["null","array"]},"is_regex":{"description":"Treat query as regex","type":["boolean","null"]},"path":{"description":"Starting directory","type":"string"},"query":{"description":"Text or regex to search","type":"string"}},"required":["excludePatterns","is_regex","path","query"],"type":"object"}},"type":"function"},{"function":{"name":"write_file","description":"Create a new file or completely overwrite an existing file with new content.","parameters":{"additionalProperties":false,"properties":{"content":{"description":"File content","type":"string"},"path":{"description":"File to write","type":"string"}},"required":["content","path"],"type":"object"}},"type":"function"},{"function":{"name":"create_directory","description":"Create one or more new directories or nested directory structures.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to create","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"},{"function":{"name":"remove_directory","description":"Remove one or more empty directories.","parameters":{"additionalProperties":false,"properties":{"paths":{"description":"Directories to remove","items":{"type":"string"},"type":"array"}},"required":["paths"],"type":"object"}},"type":"function"}],"stream":true}' url: https://api.openai.com/v1/chat/completions method: POST response: From e68e08e8a5af5245908a5311f976353854335b43 Mon Sep 17 00:00:00 2001 From: Dwin Gharibi Date: Wed, 19 Aug 2026 10:10:47 +0330 Subject: [PATCH 6/6] fix(tui): preview the refusal instead of a diff edit_file will not apply strings.Replace(..., 1) always produces a first-occurrence diff, so an ambiguous or empty oldText was rendered during confirmation as a concrete change and approved by the user before the tool declined it. The preview now runs the same EditFailureReason rule the tool does. Evaluated against the running content, so a call the tool would accept is never previewed as refused: an earlier edit may remove the duplicate that made a later one ambiguous. Both halves are covered by tests, and by controls that pass with and without the change. --- pkg/tui/components/tool/editfile/render.go | 42 ++++++++++ .../components/tool/editfile/render_test.go | 81 +++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/pkg/tui/components/tool/editfile/render.go b/pkg/tui/components/tool/editfile/render.go index e18262cb6..d2b5a2359 100644 --- a/pkg/tui/components/tool/editfile/render.go +++ b/pkg/tui/components/tool/editfile/render.go @@ -17,6 +17,7 @@ import ( "github.com/docker/docker-agent/pkg/concurrent" "github.com/docker/docker-agent/pkg/lrucache" "github.com/docker/docker-agent/pkg/tools" + "github.com/docker/docker-agent/pkg/tools/builtin/filesystem" "github.com/docker/docker-agent/pkg/tui/styles" "github.com/docker/docker-agent/pkg/tui/types" ) @@ -127,6 +128,10 @@ func renderEditFileUncached(toolCall tools.ToolCall, width int, splitView bool, return "" } + if notice := refusalNotice(args, toolStatus); notice != "" { + return notice + } + var output strings.Builder for i, edit := range args.Edits { if i > 0 { @@ -148,6 +153,43 @@ func renderEditFileUncached(toolCall tools.ToolCall, width int, splitView bool, return output.String() } +// refusalNotice returns the message to show in place of a diff when the edit +// the user is being asked to approve will be refused, or an empty string when +// the call is applicable. +// +// Without this, an ambiguous or empty oldText is previewed as a +// first-occurrence diff — strings.Replace(..., 1) always produces one — so the +// user approves a change the tool then declines, and the diff they saw was +// never the change that would have been made. +// +// Only meaningful before execution: once the tool has run, the file on disk is +// the outcome and there is nothing left to predict. +func refusalNotice(args filesystem.EditFileArgs, toolStatus types.ToolStatus) string { + if toolStatus != types.ToolStatusConfirmation { + return "" + } + + content, err := os.ReadFile(args.Path) + if err != nil { + // The tool reports unreadable paths itself; a preview must not + // second-guess it. + return "" + } + + // Evaluated against the running content exactly as handleEditFile does, so + // a preview never refuses a call the tool would accept: an earlier edit may + // legitimately remove a duplicate that made a later one ambiguous. + running := string(content) + for i, edit := range args.Edits { + if reason := filesystem.EditFailureReason(running, edit); reason != "" { + return styles.ErrorStyle.Render( + fmt.Sprintf("This call will be refused — Edit %d: %s", i+1, reason)) + } + running = strings.Replace(running, edit.OldText, edit.NewText, 1) + } + return "" +} + // countDiffLines returns the number of added and removed lines for the edit. // Results are cached per tool call since arguments are immutable. func countDiffLines(toolCall tools.ToolCall, _ types.ToolStatus) (added, removed int) { diff --git a/pkg/tui/components/tool/editfile/render_test.go b/pkg/tui/components/tool/editfile/render_test.go index e92cc32c9..3fd561ef7 100644 --- a/pkg/tui/components/tool/editfile/render_test.go +++ b/pkg/tui/components/tool/editfile/render_test.go @@ -149,3 +149,84 @@ func TestRenderEditFile_MissingFileReturnsEmptyDiff(t *testing.T) { _ = renderEditFile(toolCall, 100, false, types.ToolStatusCompleted) }) } + +// An edit whose oldText matches more than once is refused by the tool, but +// strings.Replace(..., 1) always produces a first-occurrence diff — so without +// the preview check the user approves a change that never happens, having been +// shown a diff that was never on offer. +func TestRenderEditFile_ConfirmationPreviewsRefusal(t *testing.T) { + t.Parallel() + dir := t.TempDir() + path := filepath.Join(dir, "conf.py") + + const original = "def dev():\n debug = True\n\ndef prod():\n debug = True\n" + require.NoError(t, os.WriteFile(path, []byte(original), 0o644)) + + render := func(t *testing.T, id string, edits []map[string]string, status types.ToolStatus) string { + t.Helper() + encoded, err := json.Marshal(map[string]any{"path": path, "edits": edits}) + require.NoError(t, err) + toolCall := tools.ToolCall{ + ID: id, + Function: tools.FunctionCall{Name: "edit_file", Arguments: string(encoded)}, + } + InvalidateCaches() + t.Cleanup(InvalidateCaches) + return ansi.Strip(renderEditFile(toolCall, 120, false, status)) + } + + t.Run("ambiguous edit shows the refusal instead of a diff", func(t *testing.T) { + out := render(t, "test-refusal-ambiguous", + []map[string]string{{"oldText": " debug = True", "newText": " debug = False"}}, + types.ToolStatusConfirmation) + + assert.Contains(t, out, "will be refused") + assert.Contains(t, out, "appears 2 times") + assert.NotContains(t, out, "debug = False", + "a diff the tool will not apply must not be shown") + }) + + t.Run("empty oldText shows the refusal", func(t *testing.T) { + out := render(t, "test-refusal-empty", + []map[string]string{{"oldText": "", "newText": "INJECTED"}}, + types.ToolStatusConfirmation) + + assert.Contains(t, out, "oldText must not be empty") + assert.NotContains(t, out, "INJECTED") + }) + + // Control: the preview must not refuse what the tool would accept. The + // first edit removes the duplicate that made the second one ambiguous, so + // the reason has to be evaluated against the running content. + t.Run("an earlier edit resolving a later ambiguity still previews", func(t *testing.T) { + out := render(t, "test-refusal-chained", + []map[string]string{ + {"oldText": "def dev():\n debug = True", "newText": "def dev():\n debug = None"}, + {"oldText": " debug = True", "newText": " debug = False"}, + }, + types.ToolStatusConfirmation) + + assert.NotContains(t, out, "will be refused") + assert.NotEmpty(t, out) + }) + + // Control: an unambiguous edit is unaffected. + t.Run("a unique edit previews normally", func(t *testing.T) { + out := render(t, "test-refusal-unique", + []map[string]string{{"oldText": "def prod():\n debug = True", "newText": "def prod():\n debug = False"}}, + types.ToolStatusConfirmation) + + assert.NotContains(t, out, "will be refused") + assert.Contains(t, out, "debug = False") + }) + + // Control: after execution the file on disk is the outcome; there is + // nothing left to predict and the diff must render as before. + t.Run("a completed call is never previewed as refused", func(t *testing.T) { + out := render(t, "test-refusal-completed", + []map[string]string{{"oldText": " debug = True", "newText": " debug = False"}}, + types.ToolStatusCompleted) + + assert.NotContains(t, out, "will be refused") + }) +}