Răsfoiți Sursa

test: test all the tool calls

Kujtim Hoxha 4 luni în urmă
părinte
comite
8fce31ead1
46 a modificat fișierele cu 2694 adăugiri și 151 ștergeri
  1. 1 1
      internal/agent/agent.go
  2. 512 8
      internal/agent/agent_test.go
  3. 4 4
      internal/agent/common_test.go
  4. 72 0
      internal/agent/testdata/TestCoderAgent/anthropic-sonnet/bash_tool.yaml
  5. 66 0
      internal/agent/testdata/TestCoderAgent/anthropic-sonnet/download_tool.yaml
  6. 72 0
      internal/agent/testdata/TestCoderAgent/anthropic-sonnet/fetch_tool.yaml
  7. 66 0
      internal/agent/testdata/TestCoderAgent/anthropic-sonnet/glob_tool.yaml
  8. 66 0
      internal/agent/testdata/TestCoderAgent/anthropic-sonnet/grep_tool.yaml
  9. 66 0
      internal/agent/testdata/TestCoderAgent/anthropic-sonnet/ls_tool.yaml
  10. 78 0
      internal/agent/testdata/TestCoderAgent/anthropic-sonnet/multiedit_tool.yaml
  11. 69 0
      internal/agent/testdata/TestCoderAgent/anthropic-sonnet/parallel_tool_calls.yaml
  12. 26 20
      internal/agent/testdata/TestCoderAgent/anthropic-sonnet/read_a_file.yaml
  13. 27 15
      internal/agent/testdata/TestCoderAgent/anthropic-sonnet/simple_test.yaml
  14. 72 0
      internal/agent/testdata/TestCoderAgent/anthropic-sonnet/sourcegraph_tool.yaml
  15. 69 0
      internal/agent/testdata/TestCoderAgent/anthropic-sonnet/update_a_file.yaml
  16. 75 0
      internal/agent/testdata/TestCoderAgent/anthropic-sonnet/write_tool.yaml
  17. 59 0
      internal/agent/testdata/TestCoderAgent/openai-gpt-5/bash_tool.yaml
  18. 61 0
      internal/agent/testdata/TestCoderAgent/openai-gpt-5/download_tool.yaml
  19. 69 0
      internal/agent/testdata/TestCoderAgent/openai-gpt-5/fetch_tool.yaml
  20. 65 0
      internal/agent/testdata/TestCoderAgent/openai-gpt-5/glob_tool.yaml
  21. 65 0
      internal/agent/testdata/TestCoderAgent/openai-gpt-5/grep_tool.yaml
  22. 59 0
      internal/agent/testdata/TestCoderAgent/openai-gpt-5/ls_tool.yaml
  23. 65 0
      internal/agent/testdata/TestCoderAgent/openai-gpt-5/multiedit_tool.yaml
  24. 67 0
      internal/agent/testdata/TestCoderAgent/openai-gpt-5/parallel_tool_calls.yaml
  25. 41 35
      internal/agent/testdata/TestCoderAgent/openai-gpt-5/read_a_file.yaml
  26. 13 31
      internal/agent/testdata/TestCoderAgent/openai-gpt-5/simple_test.yaml
  27. 69 0
      internal/agent/testdata/TestCoderAgent/openai-gpt-5/sourcegraph_tool.yaml
  28. 67 0
      internal/agent/testdata/TestCoderAgent/openai-gpt-5/update_a_file.yaml
  29. 59 0
      internal/agent/testdata/TestCoderAgent/openai-gpt-5/write_tool.yaml
  30. 63 0
      internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/bash_tool.yaml
  31. 55 0
      internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/download_tool.yaml
  32. 57 0
      internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/fetch_tool.yaml
  33. 55 0
      internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/glob_tool.yaml
  34. 53 0
      internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/grep_tool.yaml
  35. 53 0
      internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/ls_tool.yaml
  36. 73 0
      internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/multiedit_tool.yaml
  37. 10 0
      internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/parallel_tool_calls.yaml
  38. 1 1
      internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/read_a_file.yaml
  39. 10 12
      internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/simple_test.yaml
  40. 59 0
      internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/sourcegraph_tool.yaml
  41. 10 0
      internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/update_a_file.yaml
  42. 59 0
      internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/write_tool.yaml
  43. 10 8
      internal/agent/tools/download.go
  44. 10 8
      internal/agent/tools/fetch.go
  45. 10 8
      internal/agent/tools/sourcegraph.go
  46. 6 0
      internal/shell/persistent.go

+ 1 - 1
internal/agent/agent.go

@@ -11,8 +11,8 @@ import (
 	"time"
 
 	"github.com/charmbracelet/catwalk/pkg/catwalk"
+	"github.com/charmbracelet/crush/internal/agent/tools"
 	"github.com/charmbracelet/crush/internal/csync"
-	"github.com/charmbracelet/crush/internal/llm/tools"
 	"github.com/charmbracelet/crush/internal/message"
 	"github.com/charmbracelet/crush/internal/permission"
 	"github.com/charmbracelet/crush/internal/session"

+ 512 - 8
internal/agent/agent_test.go

@@ -1,16 +1,18 @@
 package agent
 
 import (
-	"encoding/json"
-	"fmt"
+	"os"
+	"path/filepath"
 	"strings"
 	"testing"
 
 	"github.com/charmbracelet/crush/internal/agent/tools"
 	"github.com/charmbracelet/crush/internal/message"
+	"github.com/charmbracelet/crush/internal/shell"
 	"github.com/charmbracelet/fantasy/ai"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
 
 	_ "github.com/joho/godotenv/autoload"
 )
@@ -21,8 +23,7 @@ var modelPairs = []modelPair{
 	{"openrouter-kimi-k2", openRouterBuilder("moonshotai/kimi-k2-0905"), openRouterBuilder("qwen/qwen3-next-80b-a3b-instruct")},
 }
 
-func getModels(t *testing.T, pair modelPair) (ai.LanguageModel, ai.LanguageModel) {
-	r := newRecorder(t)
+func getModels(t *testing.T, r *recorder.Recorder, pair modelPair) (ai.LanguageModel, ai.LanguageModel) {
 	large, err := pair.largeModel(t, r)
 	require.NoError(t, err)
 	small, err := pair.smallModel(t, r)
@@ -31,11 +32,13 @@ func getModels(t *testing.T, pair modelPair) (ai.LanguageModel, ai.LanguageModel
 }
 
 func setupAgent(t *testing.T, pair modelPair) (SessionAgent, env) {
-	large, small := getModels(t, pair)
+	r := newRecorder(t)
+	large, small := getModels(t, r, pair)
 	env := testEnv(t)
 
 	createSimpleGoProject(t, env.workingDir)
-	agent, err := coderAgent(env, large, small)
+	agent, err := coderAgent(r, env, large, small)
+	shell.Reset(env.workingDir)
 	require.NoError(t, err)
 	return agent, env
 }
@@ -82,8 +85,6 @@ func TestCoderAgent(t *testing.T) {
 				var tcID string
 			out:
 				for _, msg := range msgs {
-					data, _ := json.Marshal(msg)
-					fmt.Println(string(data))
 					if msg.Role == message.Assistant {
 						for _, tc := range msg.ToolCalls() {
 							if tc.Name == tools.ViewToolName {
@@ -104,6 +105,509 @@ func TestCoderAgent(t *testing.T) {
 				}
 				require.True(t, foundFile)
 			})
+			t.Run("update a file", func(t *testing.T) {
+				agent, env := setupAgent(t, pair)
+
+				session, err := env.sessions.Create(t.Context(), "New Session")
+				require.NoError(t, err)
+
+				res, err := agent.Run(t.Context(), SessionAgentCall{
+					Prompt:          "update the main.go file by changing the print to say hello from crush",
+					SessionID:       session.ID,
+					MaxOutputTokens: 10000,
+				})
+				require.NoError(t, err)
+				assert.NotNil(t, res)
+
+				msgs, err := env.messages.List(t.Context(), session.ID)
+				require.NoError(t, err)
+
+				foundRead := false
+				foundWrite := false
+				var readTCID, writeTCID string
+
+				for _, msg := range msgs {
+					if msg.Role == message.Assistant {
+						for _, tc := range msg.ToolCalls() {
+							if tc.Name == tools.ViewToolName {
+								readTCID = tc.ID
+							}
+							if tc.Name == tools.EditToolName || tc.Name == tools.WriteToolName {
+								writeTCID = tc.ID
+							}
+						}
+					}
+					if msg.Role == message.Tool {
+						for _, tr := range msg.ToolResults() {
+							if tr.ToolCallID == readTCID {
+								foundRead = true
+							}
+							if tr.ToolCallID == writeTCID {
+								foundWrite = true
+							}
+						}
+					}
+				}
+
+				require.True(t, foundRead, "Expected to find a read operation")
+				require.True(t, foundWrite, "Expected to find a write operation")
+
+				mainGoPath := filepath.Join(env.workingDir, "main.go")
+				content, err := os.ReadFile(mainGoPath)
+				require.NoError(t, err)
+				require.Contains(t, strings.ToLower(string(content)), "hello from crush")
+			})
+			t.Run("bash tool", func(t *testing.T) {
+				agent, env := setupAgent(t, pair)
+
+				session, err := env.sessions.Create(t.Context(), "New Session")
+				require.NoError(t, err)
+
+				res, err := agent.Run(t.Context(), SessionAgentCall{
+					Prompt:          "use bash to create a file named test.txt with content 'hello bash'",
+					SessionID:       session.ID,
+					MaxOutputTokens: 10000,
+				})
+				require.NoError(t, err)
+				assert.NotNil(t, res)
+
+				msgs, err := env.messages.List(t.Context(), session.ID)
+				require.NoError(t, err)
+
+				foundBash := false
+				var bashTCID string
+
+				for _, msg := range msgs {
+					if msg.Role == message.Assistant {
+						for _, tc := range msg.ToolCalls() {
+							if tc.Name == tools.BashToolName {
+								bashTCID = tc.ID
+							}
+						}
+					}
+					if msg.Role == message.Tool {
+						for _, tr := range msg.ToolResults() {
+							if tr.ToolCallID == bashTCID {
+								foundBash = true
+							}
+						}
+					}
+				}
+
+				require.True(t, foundBash, "Expected to find a bash operation")
+
+				testFilePath := filepath.Join(env.workingDir, "test.txt")
+				content, err := os.ReadFile(testFilePath)
+				require.NoError(t, err)
+				require.Contains(t, string(content), "hello bash")
+			})
+			t.Run("download tool", func(t *testing.T) {
+				agent, env := setupAgent(t, pair)
+
+				session, err := env.sessions.Create(t.Context(), "New Session")
+				require.NoError(t, err)
+
+				res, err := agent.Run(t.Context(), SessionAgentCall{
+					Prompt:          "download the file from https://httpbin.org/robots.txt and save it as robots.txt",
+					SessionID:       session.ID,
+					MaxOutputTokens: 10000,
+				})
+				require.NoError(t, err)
+				assert.NotNil(t, res)
+
+				msgs, err := env.messages.List(t.Context(), session.ID)
+				require.NoError(t, err)
+
+				foundDownload := false
+				var downloadTCID string
+
+				for _, msg := range msgs {
+					if msg.Role == message.Assistant {
+						for _, tc := range msg.ToolCalls() {
+							if tc.Name == tools.DownloadToolName {
+								downloadTCID = tc.ID
+							}
+						}
+					}
+					if msg.Role == message.Tool {
+						for _, tr := range msg.ToolResults() {
+							if tr.ToolCallID == downloadTCID {
+								foundDownload = true
+							}
+						}
+					}
+				}
+
+				require.True(t, foundDownload, "Expected to find a download operation")
+
+				robotsPath := filepath.Join(env.workingDir, "robots.txt")
+				_, err = os.Stat(robotsPath)
+				require.NoError(t, err, "Expected robots.txt file to exist")
+			})
+			t.Run("fetch tool", func(t *testing.T) {
+				agent, env := setupAgent(t, pair)
+
+				session, err := env.sessions.Create(t.Context(), "New Session")
+				require.NoError(t, err)
+
+				res, err := agent.Run(t.Context(), SessionAgentCall{
+					Prompt:          "fetch the content from https://httpbin.org/html and tell me if it contains the word 'Herman'",
+					SessionID:       session.ID,
+					MaxOutputTokens: 10000,
+				})
+				require.NoError(t, err)
+				assert.NotNil(t, res)
+
+				msgs, err := env.messages.List(t.Context(), session.ID)
+				require.NoError(t, err)
+
+				foundFetch := false
+				var fetchTCID string
+
+				for _, msg := range msgs {
+					if msg.Role == message.Assistant {
+						for _, tc := range msg.ToolCalls() {
+							if tc.Name == tools.FetchToolName {
+								fetchTCID = tc.ID
+							}
+						}
+					}
+					if msg.Role == message.Tool {
+						for _, tr := range msg.ToolResults() {
+							if tr.ToolCallID == fetchTCID {
+								foundFetch = true
+							}
+						}
+					}
+				}
+
+				require.True(t, foundFetch, "Expected to find a fetch operation")
+			})
+			t.Run("glob tool", func(t *testing.T) {
+				agent, env := setupAgent(t, pair)
+
+				session, err := env.sessions.Create(t.Context(), "New Session")
+				require.NoError(t, err)
+
+				res, err := agent.Run(t.Context(), SessionAgentCall{
+					Prompt:          "use glob to find all .go files in the current directory",
+					SessionID:       session.ID,
+					MaxOutputTokens: 10000,
+				})
+				require.NoError(t, err)
+				assert.NotNil(t, res)
+
+				msgs, err := env.messages.List(t.Context(), session.ID)
+				require.NoError(t, err)
+
+				foundGlob := false
+				var globTCID string
+
+				for _, msg := range msgs {
+					if msg.Role == message.Assistant {
+						for _, tc := range msg.ToolCalls() {
+							if tc.Name == tools.GlobToolName {
+								globTCID = tc.ID
+							}
+						}
+					}
+					if msg.Role == message.Tool {
+						for _, tr := range msg.ToolResults() {
+							if tr.ToolCallID == globTCID {
+								foundGlob = true
+								require.Contains(t, tr.Content, "main.go", "Expected glob to find main.go")
+							}
+						}
+					}
+				}
+
+				require.True(t, foundGlob, "Expected to find a glob operation")
+			})
+			t.Run("grep tool", func(t *testing.T) {
+				agent, env := setupAgent(t, pair)
+
+				session, err := env.sessions.Create(t.Context(), "New Session")
+				require.NoError(t, err)
+
+				res, err := agent.Run(t.Context(), SessionAgentCall{
+					Prompt:          "use grep to search for the word 'package' in go files",
+					SessionID:       session.ID,
+					MaxOutputTokens: 10000,
+				})
+				require.NoError(t, err)
+				assert.NotNil(t, res)
+
+				msgs, err := env.messages.List(t.Context(), session.ID)
+				require.NoError(t, err)
+
+				foundGrep := false
+				var grepTCID string
+
+				for _, msg := range msgs {
+					if msg.Role == message.Assistant {
+						for _, tc := range msg.ToolCalls() {
+							if tc.Name == tools.GrepToolName {
+								grepTCID = tc.ID
+							}
+						}
+					}
+					if msg.Role == message.Tool {
+						for _, tr := range msg.ToolResults() {
+							if tr.ToolCallID == grepTCID {
+								foundGrep = true
+								require.Contains(t, tr.Content, "main.go", "Expected grep to find main.go")
+							}
+						}
+					}
+				}
+
+				require.True(t, foundGrep, "Expected to find a grep operation")
+			})
+			t.Run("ls tool", func(t *testing.T) {
+				agent, env := setupAgent(t, pair)
+
+				session, err := env.sessions.Create(t.Context(), "New Session")
+				require.NoError(t, err)
+
+				res, err := agent.Run(t.Context(), SessionAgentCall{
+					Prompt:          "use ls to list the files in the current directory",
+					SessionID:       session.ID,
+					MaxOutputTokens: 10000,
+				})
+				require.NoError(t, err)
+				assert.NotNil(t, res)
+
+				msgs, err := env.messages.List(t.Context(), session.ID)
+				require.NoError(t, err)
+
+				foundLS := false
+				var lsTCID string
+
+				for _, msg := range msgs {
+					if msg.Role == message.Assistant {
+						for _, tc := range msg.ToolCalls() {
+							if tc.Name == tools.LSToolName {
+								lsTCID = tc.ID
+							}
+						}
+					}
+					if msg.Role == message.Tool {
+						for _, tr := range msg.ToolResults() {
+							if tr.ToolCallID == lsTCID {
+								foundLS = true
+								require.Contains(t, tr.Content, "main.go", "Expected ls to list main.go")
+								require.Contains(t, tr.Content, "go.mod", "Expected ls to list go.mod")
+							}
+						}
+					}
+				}
+
+				require.True(t, foundLS, "Expected to find an ls operation")
+			})
+			t.Run("multiedit tool", func(t *testing.T) {
+				agent, env := setupAgent(t, pair)
+
+				session, err := env.sessions.Create(t.Context(), "New Session")
+				require.NoError(t, err)
+
+				res, err := agent.Run(t.Context(), SessionAgentCall{
+					Prompt:          "use multiedit to change 'Hello, World!' to 'Hello, Crush!' and add a comment '// Greeting' above the fmt.Println line in main.go",
+					SessionID:       session.ID,
+					MaxOutputTokens: 10000,
+				})
+				require.NoError(t, err)
+				assert.NotNil(t, res)
+
+				msgs, err := env.messages.List(t.Context(), session.ID)
+				require.NoError(t, err)
+
+				foundMultiEdit := false
+				var multiEditTCID string
+
+				for _, msg := range msgs {
+					if msg.Role == message.Assistant {
+						for _, tc := range msg.ToolCalls() {
+							if tc.Name == tools.MultiEditToolName {
+								multiEditTCID = tc.ID
+							}
+						}
+					}
+					if msg.Role == message.Tool {
+						for _, tr := range msg.ToolResults() {
+							if tr.ToolCallID == multiEditTCID {
+								foundMultiEdit = true
+							}
+						}
+					}
+				}
+
+				require.True(t, foundMultiEdit, "Expected to find a multiedit operation")
+
+				mainGoPath := filepath.Join(env.workingDir, "main.go")
+				content, err := os.ReadFile(mainGoPath)
+				require.NoError(t, err)
+				require.Contains(t, string(content), "Hello, Crush!", "Expected file to contain 'Hello, Crush!'")
+			})
+			t.Run("sourcegraph tool", func(t *testing.T) {
+				agent, env := setupAgent(t, pair)
+
+				session, err := env.sessions.Create(t.Context(), "New Session")
+				require.NoError(t, err)
+
+				res, err := agent.Run(t.Context(), SessionAgentCall{
+					Prompt:          "use sourcegraph to search for 'func main' in Go repositories",
+					SessionID:       session.ID,
+					MaxOutputTokens: 10000,
+				})
+				require.NoError(t, err)
+				assert.NotNil(t, res)
+
+				msgs, err := env.messages.List(t.Context(), session.ID)
+				require.NoError(t, err)
+
+				foundSourcegraph := false
+				var sourcegraphTCID string
+
+				for _, msg := range msgs {
+					if msg.Role == message.Assistant {
+						for _, tc := range msg.ToolCalls() {
+							if tc.Name == tools.SourcegraphToolName {
+								sourcegraphTCID = tc.ID
+							}
+						}
+					}
+					if msg.Role == message.Tool {
+						for _, tr := range msg.ToolResults() {
+							if tr.ToolCallID == sourcegraphTCID {
+								foundSourcegraph = true
+							}
+						}
+					}
+				}
+
+				require.True(t, foundSourcegraph, "Expected to find a sourcegraph operation")
+			})
+			t.Run("write tool", func(t *testing.T) {
+				agent, env := setupAgent(t, pair)
+
+				session, err := env.sessions.Create(t.Context(), "New Session")
+				require.NoError(t, err)
+
+				res, err := agent.Run(t.Context(), SessionAgentCall{
+					Prompt:          "use write to create a new file called config.json with content '{\"name\": \"test\", \"version\": \"1.0.0\"}'",
+					SessionID:       session.ID,
+					MaxOutputTokens: 10000,
+				})
+				require.NoError(t, err)
+				assert.NotNil(t, res)
+
+				msgs, err := env.messages.List(t.Context(), session.ID)
+				require.NoError(t, err)
+
+				foundWrite := false
+				var writeTCID string
+
+				for _, msg := range msgs {
+					if msg.Role == message.Assistant {
+						for _, tc := range msg.ToolCalls() {
+							if tc.Name == tools.WriteToolName {
+								writeTCID = tc.ID
+							}
+						}
+					}
+					if msg.Role == message.Tool {
+						for _, tr := range msg.ToolResults() {
+							if tr.ToolCallID == writeTCID {
+								foundWrite = true
+							}
+						}
+					}
+				}
+
+				require.True(t, foundWrite, "Expected to find a write operation")
+
+				configPath := filepath.Join(env.workingDir, "config.json")
+				content, err := os.ReadFile(configPath)
+				require.NoError(t, err)
+				require.Contains(t, string(content), "test", "Expected config.json to contain 'test'")
+				require.Contains(t, string(content), "1.0.0", "Expected config.json to contain '1.0.0'")
+			})
+			t.Run("parallel tool calls", func(t *testing.T) {
+				agent, env := setupAgent(t, pair)
+
+				session, err := env.sessions.Create(t.Context(), "New Session")
+				require.NoError(t, err)
+
+				res, err := agent.Run(t.Context(), SessionAgentCall{
+					Prompt:          "use glob to find all .go files and use ls to list the current directory, it is very important that you run both tool calls in parallel",
+					SessionID:       session.ID,
+					MaxOutputTokens: 10000,
+				})
+				require.NoError(t, err)
+				assert.NotNil(t, res)
+
+				msgs, err := env.messages.List(t.Context(), session.ID)
+				require.NoError(t, err)
+
+				var assistantMsg *message.Message
+				var toolMsgs []message.Message
+
+				for _, msg := range msgs {
+					if msg.Role == message.Assistant && len(msg.ToolCalls()) > 0 {
+						assistantMsg = &msg
+					}
+					if msg.Role == message.Tool {
+						toolMsgs = append(toolMsgs, msg)
+					}
+				}
+
+				require.NotNil(t, assistantMsg, "Expected to find an assistant message with tool calls")
+				require.NotNil(t, toolMsgs, "Expected to find a tool message")
+
+				toolCalls := assistantMsg.ToolCalls()
+				require.GreaterOrEqual(t, len(toolCalls), 2, "Expected at least 2 tool calls in parallel")
+
+				foundGlob := false
+				foundLS := false
+				var globTCID, lsTCID string
+
+				for _, tc := range toolCalls {
+					if tc.Name == tools.GlobToolName {
+						foundGlob = true
+						globTCID = tc.ID
+					}
+					if tc.Name == tools.LSToolName {
+						foundLS = true
+						lsTCID = tc.ID
+					}
+				}
+
+				require.True(t, foundGlob, "Expected to find a glob tool call")
+				require.True(t, foundLS, "Expected to find an ls tool call")
+
+				require.GreaterOrEqual(t, len(toolMsgs), 2, "Expected at least 2 tool results in the same message")
+
+				foundGlobResult := false
+				foundLSResult := false
+
+				for _, msg := range toolMsgs {
+					for _, tr := range msg.ToolResults() {
+						if tr.ToolCallID == globTCID {
+							foundGlobResult = true
+							require.Contains(t, tr.Content, "main.go", "Expected glob result to contain main.go")
+							require.False(t, tr.IsError, "Expected glob result to not be an error")
+						}
+						if tr.ToolCallID == lsTCID {
+							foundLSResult = true
+							require.Contains(t, tr.Content, "main.go", "Expected ls result to contain main.go")
+							require.False(t, tr.IsError, "Expected ls result to not be an error")
+						}
+					}
+				}
+
+				require.True(t, foundGlobResult, "Expected to find glob tool result")
+				require.True(t, foundLSResult, "Expected to find ls tool result")
+			})
 		})
 	}
 }

+ 4 - 4
internal/agent/common_test.go

@@ -129,7 +129,7 @@ func testSessionAgent(env env, large, small ai.LanguageModel, systemPrompt strin
 	return agent
 }
 
-func coderAgent(env env, large, small ai.LanguageModel) (SessionAgent, error) {
+func coderAgent(r *recorder.Recorder, env env, large, small ai.LanguageModel) (SessionAgent, error) {
 	fixedTime := func() time.Time {
 		t, _ := time.Parse("1/2/2006", "1/1/2025")
 		return t
@@ -149,14 +149,14 @@ func coderAgent(env env, large, small ai.LanguageModel) (SessionAgent, error) {
 	}
 	allTools := []ai.AgentTool{
 		tools.NewBashTool(env.permissions, env.workingDir, cfg.Options.Attribution),
-		tools.NewDownloadTool(env.permissions, env.workingDir),
+		tools.NewDownloadTool(env.permissions, env.workingDir, r.GetDefaultClient()),
 		tools.NewEditTool(env.lspClients, env.permissions, env.history, env.workingDir),
 		tools.NewMultiEditTool(env.lspClients, env.permissions, env.history, env.workingDir),
-		tools.NewFetchTool(env.permissions, env.workingDir),
+		tools.NewFetchTool(env.permissions, env.workingDir, r.GetDefaultClient()),
 		tools.NewGlobTool(env.workingDir),
 		tools.NewGrepTool(env.workingDir),
 		tools.NewLsTool(env.permissions, env.workingDir),
-		tools.NewSourcegraphTool(),
+		tools.NewSourcegraphTool(r.GetDefaultClient()),
 		tools.NewViewTool(env.lspClients, env.permissions, env.workingDir),
 		tools.NewWriteTool(env.lspClients, env.permissions, env.history, env.workingDir),
 	}

Fișier diff suprimat deoarece este prea mare
+ 72 - 0
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/bash_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 66 - 0
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/download_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 72 - 0
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/fetch_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 66 - 0
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/glob_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 66 - 0
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/grep_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 66 - 0
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/ls_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 78 - 0
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/multiedit_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 69 - 0
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/parallel_tool_calls.yaml


Fișier diff suprimat deoarece este prea mare
+ 26 - 20
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/read_a_file.yaml


+ 27 - 15
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/simple_test.yaml

@@ -25,29 +25,41 @@ interactions:
     content_length: -1
     body: |+
       event: message_start
-      data: {"type":"message_start","message":{"id":"msg_01EA1yHe2hGWqG98PVSSf5s8","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":108,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard"}} }
+      data: {"type":"message_start","message":{"id":"msg_01Pn5WLdGCLP5aKCti1Y47pz","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":108,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}}       }
 
       event: content_block_start
-      data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}           }
+      data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}      }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Greeting"}               }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"First"}           }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Contact"}         }
+
+      event: ping
+      data: {"type": "ping"}
 
       event: content_block_stop
-      data: {"type":"content_block_stop","index":0    }
+      data: {"type":"content_block_stop","index":0               }
+
+      event: ping
+      data: {"type": "ping"}
+
+      event: ping
+      data: {"type": "ping"}
 
       event: message_delta
-      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":108,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5}              }
+      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":108,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5}               }
 
       event: message_stop
-      data: {"type":"message_stop"      }
+      data: {"type":"message_stop"       }
 
     headers:
       Content-Type:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 642.344708ms
+    duration: 692.799334ms
 - id: 1
   request:
     proto: HTTP/1.1
@@ -72,22 +84,22 @@ interactions:
     content_length: -1
     body: |+
       event: message_start
-      data: {"type":"message_start","message":{"id":"msg_01S99d6CmzZeEvf987aG9ksX","type":"message","role":"assistant","model":"claude-sonnet-4-5-20250929","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":8645,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":8645,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard"}}   }
+      data: {"type":"message_start","message":{"id":"msg_015DTCS4wTuYNBhwJXZtLJLm","type":"message","role":"assistant","model":"claude-sonnet-4-5-20250929","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":8645,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":8645,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard"}}              }
 
       event: content_block_start
-      data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}   }
+      data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}     }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello! How"}   }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello! How"}}
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" can I help you today?"}}
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" can I help you today?"}           }
 
       event: ping
       data: {"type": "ping"}
 
       event: content_block_stop
-      data: {"type":"content_block_stop","index":0          }
+      data: {"type":"content_block_stop","index":0         }
 
       event: ping
       data: {"type": "ping"}
@@ -96,14 +108,14 @@ interactions:
       data: {"type": "ping"}
 
       event: message_delta
-      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":3,"cache_creation_input_tokens":8645,"cache_read_input_tokens":0,"output_tokens":12}}
+      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":3,"cache_creation_input_tokens":8645,"cache_read_input_tokens":0,"output_tokens":12}            }
 
       event: message_stop
-      data: {"type":"message_stop"      }
+      data: {"type":"message_stop"             }
 
     headers:
       Content-Type:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 2.390267667s
+    duration: 3.558943042s

Fișier diff suprimat deoarece este prea mare
+ 72 - 0
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/sourcegraph_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 69 - 0
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/update_a_file.yaml


Fișier diff suprimat deoarece este prea mare
+ 75 - 0
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/write_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 59 - 0
internal/agent/testdata/TestCoderAgent/openai-gpt-5/bash_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 61 - 0
internal/agent/testdata/TestCoderAgent/openai-gpt-5/download_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 69 - 0
internal/agent/testdata/TestCoderAgent/openai-gpt-5/fetch_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 65 - 0
internal/agent/testdata/TestCoderAgent/openai-gpt-5/glob_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 65 - 0
internal/agent/testdata/TestCoderAgent/openai-gpt-5/grep_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 59 - 0
internal/agent/testdata/TestCoderAgent/openai-gpt-5/ls_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 65 - 0
internal/agent/testdata/TestCoderAgent/openai-gpt-5/multiedit_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 67 - 0
internal/agent/testdata/TestCoderAgent/openai-gpt-5/parallel_tool_calls.yaml


Fișier diff suprimat deoarece este prea mare
+ 41 - 35
internal/agent/testdata/TestCoderAgent/openai-gpt-5/read_a_file.yaml


+ 13 - 31
internal/agent/testdata/TestCoderAgent/openai-gpt-5/simple_test.yaml

@@ -24,25 +24,19 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"chatcmpl-CLnyZUC3HECSNiCyPUyFNYk3jkToU","object":"chat.completion.chunk","created":1759313031,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"FQ2C1HyimwsSIJ"}
+      data: {"id":"chatcmpl-CLqSPM5xEBAzlWvxt8DMeqRGaDv9l","object":"chat.completion.chunk","created":1759322569,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_f33640a400","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"2jyVpvWydcoDzR"}
 
-      data: {"id":"chatcmpl-CLnyZUC3HECSNiCyPUyFNYk3jkToU","object":"chat.completion.chunk","created":1759313031,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"User"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"LIC7mLokOZ9E"}
+      data: {"id":"chatcmpl-CLqSPM5xEBAzlWvxt8DMeqRGaDv9l","object":"chat.completion.chunk","created":1759322569,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_f33640a400","choices":[{"index":0,"delta":{"content":"User"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"BchqB0J32Wsd"}
 
-      data: {"id":"chatcmpl-CLnyZUC3HECSNiCyPUyFNYk3jkToU","object":"chat.completion.chunk","created":1759313031,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Gre"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"r7SBrJb3ou5J"}
+      data: {"id":"chatcmpl-CLqSPM5xEBAzlWvxt8DMeqRGaDv9l","object":"chat.completion.chunk","created":1759322569,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_f33640a400","choices":[{"index":0,"delta":{"content":"'s"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"9vvcSlmIx5o9qW"}
 
-      data: {"id":"chatcmpl-CLnyZUC3HECSNiCyPUyFNYk3jkToU","object":"chat.completion.chunk","created":1759313031,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"ets"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"zUr2uM9B9K3ob"}
+      data: {"id":"chatcmpl-CLqSPM5xEBAzlWvxt8DMeqRGaDv9l","object":"chat.completion.chunk","created":1759322569,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_f33640a400","choices":[{"index":0,"delta":{"content":" Initial"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"GzmBljPi"}
 
-      data: {"id":"chatcmpl-CLnyZUC3HECSNiCyPUyFNYk3jkToU","object":"chat.completion.chunk","created":1759313031,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" with"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"csC49xwKSOL"}
+      data: {"id":"chatcmpl-CLqSPM5xEBAzlWvxt8DMeqRGaDv9l","object":"chat.completion.chunk","created":1759322569,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_f33640a400","choices":[{"index":0,"delta":{"content":" Greeting"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"BmT9dMg"}
 
-      data: {"id":"chatcmpl-CLnyZUC3HECSNiCyPUyFNYk3jkToU","object":"chat.completion.chunk","created":1759313031,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"U2SfAZTiySGrTd"}
+      data: {"id":"chatcmpl-CLqSPM5xEBAzlWvxt8DMeqRGaDv9l","object":"chat.completion.chunk","created":1759322569,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_f33640a400","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"kMj5pI727f"}
 
-      data: {"id":"chatcmpl-CLnyZUC3HECSNiCyPUyFNYk3jkToU","object":"chat.completion.chunk","created":1759313031,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Simple"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"kF2mXsyQE"}
-
-      data: {"id":"chatcmpl-CLnyZUC3HECSNiCyPUyFNYk3jkToU","object":"chat.completion.chunk","created":1759313031,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Hello"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"2Ffvui00sv"}
-
-      data: {"id":"chatcmpl-CLnyZUC3HECSNiCyPUyFNYk3jkToU","object":"chat.completion.chunk","created":1759313031,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"wj1wg1XKPv"}
-
-      data: {"id":"chatcmpl-CLnyZUC3HECSNiCyPUyFNYk3jkToU","object":"chat.completion.chunk","created":1759313031,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":109,"completion_tokens":7,"total_tokens":116,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"hss1b1Nj1zXerq"}
+      data: {"id":"chatcmpl-CLqSPM5xEBAzlWvxt8DMeqRGaDv9l","object":"chat.completion.chunk","created":1759322569,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_f33640a400","choices":[],"usage":{"prompt_tokens":109,"completion_tokens":4,"total_tokens":113,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"CZWaBIXTMZPzE4"}
 
       data: [DONE]
 
@@ -51,7 +45,7 @@ interactions:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 1.173388541s
+    duration: 589.427875ms
 - id: 1
   request:
     proto: HTTP/1.1
@@ -75,25 +69,13 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"chatcmpl-CLnyZweXlaWIhQPdgTTMjKH0ieg43","object":"chat.completion.chunk","created":1759313031,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"finish_reason":null}],"usage":null,"obfuscation":"BlAf6hXt5L"}
-
-      data: {"id":"chatcmpl-CLnyZweXlaWIhQPdgTTMjKH0ieg43","object":"chat.completion.chunk","created":1759313031,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"Hi"},"finish_reason":null}],"usage":null,"obfuscation":"lmC5EDku3L"}
-
-      data: {"id":"chatcmpl-CLnyZweXlaWIhQPdgTTMjKH0ieg43","object":"chat.completion.chunk","created":1759313031,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"!"},"finish_reason":null}],"usage":null,"obfuscation":"R4SN5zRlnTY"}
-
-      data: {"id":"chatcmpl-CLnyZweXlaWIhQPdgTTMjKH0ieg43","object":"chat.completion.chunk","created":1759313031,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" How"},"finish_reason":null}],"usage":null,"obfuscation":"YoJvJi1Y"}
-
-      data: {"id":"chatcmpl-CLnyZweXlaWIhQPdgTTMjKH0ieg43","object":"chat.completion.chunk","created":1759313031,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" can"},"finish_reason":null}],"usage":null,"obfuscation":"EHjiUEMv"}
-
-      data: {"id":"chatcmpl-CLnyZweXlaWIhQPdgTTMjKH0ieg43","object":"chat.completion.chunk","created":1759313031,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" I"},"finish_reason":null}],"usage":null,"obfuscation":"tk00iUY5TP"}
-
-      data: {"id":"chatcmpl-CLnyZweXlaWIhQPdgTTMjKH0ieg43","object":"chat.completion.chunk","created":1759313031,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":" help"},"finish_reason":null}],"usage":null,"obfuscation":"8tE8LXF"}
+      data: {"id":"chatcmpl-CLqSPD68gpvvHjIs88ayEM0SmTCLC","object":"chat.completion.chunk","created":1759322569,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"finish_reason":null}],"usage":null,"obfuscation":"F6FsxCnM3u"}
 
-      data: {"id":"chatcmpl-CLnyZweXlaWIhQPdgTTMjKH0ieg43","object":"chat.completion.chunk","created":1759313031,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"?"},"finish_reason":null}],"usage":null,"obfuscation":"NukrNEtVmmp"}
+      data: {"id":"chatcmpl-CLqSPD68gpvvHjIs88ayEM0SmTCLC","object":"chat.completion.chunk","created":1759322569,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"Hi"},"finish_reason":null}],"usage":null,"obfuscation":"bOfHSbC8pG"}
 
-      data: {"id":"chatcmpl-CLnyZweXlaWIhQPdgTTMjKH0ieg43","object":"chat.completion.chunk","created":1759313031,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":null,"obfuscation":"RSH2sU"}
+      data: {"id":"chatcmpl-CLqSPD68gpvvHjIs88ayEM0SmTCLC","object":"chat.completion.chunk","created":1759322569,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":null,"obfuscation":"xuyCQb"}
 
-      data: {"id":"chatcmpl-CLnyZweXlaWIhQPdgTTMjKH0ieg43","object":"chat.completion.chunk","created":1759313031,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[],"usage":{"prompt_tokens":6542,"completion_tokens":80,"total_tokens":6622,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":64,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"hmNsuX"}
+      data: {"id":"chatcmpl-CLqSPD68gpvvHjIs88ayEM0SmTCLC","object":"chat.completion.chunk","created":1759322569,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[],"usage":{"prompt_tokens":6542,"completion_tokens":74,"total_tokens":6616,"prompt_tokens_details":{"cached_tokens":6528,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":64,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"LmL"}
 
       data: [DONE]
 
@@ -102,4 +84,4 @@ interactions:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 4.041598333s
+    duration: 3.666814459s

Fișier diff suprimat deoarece este prea mare
+ 69 - 0
internal/agent/testdata/TestCoderAgent/openai-gpt-5/sourcegraph_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 67 - 0
internal/agent/testdata/TestCoderAgent/openai-gpt-5/update_a_file.yaml


Fișier diff suprimat deoarece este prea mare
+ 59 - 0
internal/agent/testdata/TestCoderAgent/openai-gpt-5/write_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 63 - 0
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/bash_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 55 - 0
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/download_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 57 - 0
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/fetch_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 55 - 0
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/glob_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 53 - 0
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/grep_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 53 - 0
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/ls_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 73 - 0
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/multiedit_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 10 - 0
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/parallel_tool_calls.yaml


Fișier diff suprimat deoarece este prea mare
+ 1 - 1
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/read_a_file.yaml


+ 10 - 12
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/simple_test.yaml

@@ -24,13 +24,13 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"gen-1759316452-zjAj0sEKiIFFAKMkb9ry","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1759316452,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1759322621-088y2ZDJ2qWERmy5LEKL","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1759322621,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1759316452-zjAj0sEKiIFFAKMkb9ry","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1759316452,"choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1759322621-088y2ZDJ2qWERmy5LEKL","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1759322621,"choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1759316452-zjAj0sEKiIFFAKMkb9ry","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1759316452,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
+      data: {"id":"gen-1759322621-088y2ZDJ2qWERmy5LEKL","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1759322621,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
 
-      data: {"id":"gen-1759316452-zjAj0sEKiIFFAKMkb9ry","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1759316452,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":113,"completion_tokens":2,"total_tokens":115,"cost":0.0000129,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.0000113,"upstream_inference_completions_cost":0.0000016},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
+      data: {"id":"gen-1759322621-088y2ZDJ2qWERmy5LEKL","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1759322621,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":113,"completion_tokens":2,"total_tokens":115,"cost":0.0000129,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.0000113,"upstream_inference_completions_cost":0.0000016},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
 
       data: [DONE]
 
@@ -39,7 +39,7 @@ interactions:
       - text/event-stream
     status: 200 OK
     code: 200
-    duration: 1.246887958s
+    duration: 1.105230416s
 - id: 1
   request:
     proto: HTTP/1.1
@@ -63,15 +63,13 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"gen-1759316452-0gJYswImYao5Of0iy7j1","provider":"Moonshot AI","model":"moonshotai/kimi-k2-0905","object":"chat.completion.chunk","created":1759316452,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":"fpv0_ef28f882"}
+      data: {"id":"gen-1759322621-TFyRE7QqvCi8A7vVSHKy","provider":"Novita","model":"moonshotai/kimi-k2-0905","object":"chat.completion.chunk","created":1759322621,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":"fpv0_ef28f882"}
 
-      data: {"id":"gen-1759316452-0gJYswImYao5Of0iy7j1","provider":"Moonshot AI","model":"moonshotai/kimi-k2-0905","object":"chat.completion.chunk","created":1759316452,"choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":"fpv0_ef28f882"}
+      data: {"id":"gen-1759322621-TFyRE7QqvCi8A7vVSHKy","provider":"Novita","model":"moonshotai/kimi-k2-0905","object":"chat.completion.chunk","created":1759322621,"choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":"fpv0_ef28f882"}
 
-      data: {"id":"gen-1759316452-0gJYswImYao5Of0iy7j1","provider":"Moonshot AI","model":"moonshotai/kimi-k2-0905","object":"chat.completion.chunk","created":1759316452,"choices":[{"index":0,"delta":{"role":"assistant","content":"."},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":"fpv0_ef28f882"}
+      data: {"id":"gen-1759322621-TFyRE7QqvCi8A7vVSHKy","provider":"Novita","model":"moonshotai/kimi-k2-0905","object":"chat.completion.chunk","created":1759322621,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}],"system_fingerprint":"fpv0_ef28f882"}
 
-      data: {"id":"gen-1759316452-0gJYswImYao5Of0iy7j1","provider":"Moonshot AI","model":"moonshotai/kimi-k2-0905","object":"chat.completion.chunk","created":1759316452,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}],"system_fingerprint":"fpv0_ef28f882"}
-
-      data: {"id":"gen-1759316452-0gJYswImYao5Of0iy7j1","provider":"Moonshot AI","model":"moonshotai/kimi-k2-0905","object":"chat.completion.chunk","created":1759316452,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":7552,"completion_tokens":3,"total_tokens":7555,"cost":0.0045387,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.0045312,"upstream_inference_completions_cost":0.0000075},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
+      data: {"id":"gen-1759322621-TFyRE7QqvCi8A7vVSHKy","provider":"Novita","model":"moonshotai/kimi-k2-0905","object":"chat.completion.chunk","created":1759322621,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":7552,"completion_tokens":2,"total_tokens":7554,"cost":0.0045362,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.0045312,"upstream_inference_completions_cost":0.000005},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
 
       data: [DONE]
 
@@ -80,4 +78,4 @@ interactions:
       - text/event-stream
     status: 200 OK
     code: 200
-    duration: 4.667991458s
+    duration: 2.331369875s

Fișier diff suprimat deoarece este prea mare
+ 59 - 0
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/sourcegraph_tool.yaml


Fișier diff suprimat deoarece este prea mare
+ 10 - 0
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/update_a_file.yaml


Fișier diff suprimat deoarece este prea mare
+ 59 - 0
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/write_tool.yaml


+ 10 - 8
internal/agent/tools/download.go

@@ -32,14 +32,16 @@ const DownloadToolName = "download"
 //go:embed download.md
 var downloadDescription []byte
 
-func NewDownloadTool(permissions permission.Service, workingDir string) ai.AgentTool {
-	client := &http.Client{
-		Timeout: 5 * time.Minute, // Default 5 minute timeout for downloads
-		Transport: &http.Transport{
-			MaxIdleConns:        100,
-			MaxIdleConnsPerHost: 10,
-			IdleConnTimeout:     90 * time.Second,
-		},
+func NewDownloadTool(permissions permission.Service, workingDir string, client *http.Client) ai.AgentTool {
+	if client == nil {
+		client = &http.Client{
+			Timeout: 5 * time.Minute, // Default 5 minute timeout for downloads
+			Transport: &http.Transport{
+				MaxIdleConns:        100,
+				MaxIdleConnsPerHost: 10,
+				IdleConnTimeout:     90 * time.Second,
+			},
+		}
 	}
 	return ai.NewAgentTool(
 		DownloadToolName,

+ 10 - 8
internal/agent/tools/fetch.go

@@ -39,14 +39,16 @@ const FetchToolName = "fetch"
 //go:embed fetch.md
 var fetchDescription []byte
 
-func NewFetchTool(permissions permission.Service, workingDir string) ai.AgentTool {
-	client := &http.Client{
-		Timeout: 30 * time.Second,
-		Transport: &http.Transport{
-			MaxIdleConns:        100,
-			MaxIdleConnsPerHost: 10,
-			IdleConnTimeout:     90 * time.Second,
-		},
+func NewFetchTool(permissions permission.Service, workingDir string, client *http.Client) ai.AgentTool {
+	if client == nil {
+		client = &http.Client{
+			Timeout: 30 * time.Second,
+			Transport: &http.Transport{
+				MaxIdleConns:        100,
+				MaxIdleConnsPerHost: 10,
+				IdleConnTimeout:     90 * time.Second,
+			},
+		}
 	}
 
 	return ai.NewAgentTool(

+ 10 - 8
internal/agent/tools/sourcegraph.go

@@ -31,14 +31,16 @@ const SourcegraphToolName = "sourcegraph"
 //go:embed sourcegraph.md
 var sourcegraphDescription []byte
 
-func NewSourcegraphTool() ai.AgentTool {
-	client := &http.Client{
-		Timeout: 30 * time.Second,
-		Transport: &http.Transport{
-			MaxIdleConns:        100,
-			MaxIdleConnsPerHost: 10,
-			IdleConnTimeout:     90 * time.Second,
-		},
+func NewSourcegraphTool(client *http.Client) ai.AgentTool {
+	if client == nil {
+		client = &http.Client{
+			Timeout: 30 * time.Second,
+			Transport: &http.Transport{
+				MaxIdleConns:        100,
+				MaxIdleConnsPerHost: 10,
+				IdleConnTimeout:     90 * time.Second,
+			},
+		}
 	}
 	return ai.NewAgentTool(
 		SourcegraphToolName,

+ 6 - 0
internal/shell/persistent.go

@@ -29,6 +29,12 @@ func GetPersistentShell(cwd string) *PersistentShell {
 	return shellInstance
 }
 
+// INFO: only used for tests
+func Reset(cwd string) {
+	once = sync.Once{}
+	_ = GetPersistentShell(cwd)
+}
+
 // slog.dapter adapts the internal slog.package to the Logger interface
 type loggingAdapter struct{}
 

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff