Merge branch 'main' into ui

Ayman Bagabas created

Change summary

.github/cla-signatures.json                                                        |  16 
CRUSH.md                                                                           |   7 
README.md                                                                          |   6 
go.mod                                                                             |  26 
go.sum                                                                             |  53 
internal/agent/agent.go                                                            |  66 
internal/agent/agent_tool.go                                                       |   4 
internal/agent/agentic_fetch_tool.go                                               | 217 
internal/agent/coordinator.go                                                      |  37 
internal/agent/templates/agentic_fetch.md                                          |  51 
internal/agent/templates/agentic_fetch_prompt.md.tpl                               |  45 
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/bash_tool.yaml             |  27 
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/download_tool.yaml         |  24 
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/fetch_tool.yaml            |  27 
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/glob_tool.yaml             |  25 
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/grep_tool.yaml             |  23 
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/ls_tool.yaml               |  26 
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/multiedit_tool.yaml        |  22 
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/parallel_tool_calls.yaml   |  29 
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/read_a_file.yaml           |  22 
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/simple_test.yaml           |  27 
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/sourcegraph_tool.yaml      |  25 
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/update_a_file.yaml         |  22 
internal/agent/testdata/TestCoderAgent/anthropic-sonnet/write_tool.yaml            |  27 
internal/agent/testdata/TestCoderAgent/openai-gpt-5/bash_tool.yaml                 |  24 
internal/agent/testdata/TestCoderAgent/openai-gpt-5/download_tool.yaml             |  24 
internal/agent/testdata/TestCoderAgent/openai-gpt-5/fetch_tool.yaml                |  30 
internal/agent/testdata/TestCoderAgent/openai-gpt-5/glob_tool.yaml                 |  28 
internal/agent/testdata/TestCoderAgent/openai-gpt-5/grep_tool.yaml                 |  32 
internal/agent/testdata/TestCoderAgent/openai-gpt-5/ls_tool.yaml                   |  30 
internal/agent/testdata/TestCoderAgent/openai-gpt-5/multiedit_tool.yaml            |  34 
internal/agent/testdata/TestCoderAgent/openai-gpt-5/parallel_tool_calls.yaml       |  30 
internal/agent/testdata/TestCoderAgent/openai-gpt-5/read_a_file.yaml               |  18 
internal/agent/testdata/TestCoderAgent/openai-gpt-5/simple_test.yaml               |  14 
internal/agent/testdata/TestCoderAgent/openai-gpt-5/sourcegraph_tool.yaml          |  34 
internal/agent/testdata/TestCoderAgent/openai-gpt-5/update_a_file.yaml             |  30 
internal/agent/testdata/TestCoderAgent/openai-gpt-5/write_tool.yaml                |  22 
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/bash_tool.yaml           |  22 
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/download_tool.yaml       |  30 
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/fetch_tool.yaml          |   3 
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/glob_tool.yaml           |  20 
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/grep_tool.yaml           |  26 
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/ls_tool.yaml             |  24 
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/multiedit_tool.yaml      |  20 
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/parallel_tool_calls.yaml |  22 
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/read_a_file.yaml         |   2 
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/simple_test.yaml         |  14 
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/sourcegraph_tool.yaml    |  30 
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/update_a_file.yaml       |  18 
internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/write_tool.yaml          |  18 
internal/agent/testdata/TestCoderAgent/zai-glm4.6/bash_tool.yaml                   |  20 
internal/agent/testdata/TestCoderAgent/zai-glm4.6/download_tool.yaml               |  22 
internal/agent/testdata/TestCoderAgent/zai-glm4.6/fetch_tool.yaml                  |   3 
internal/agent/testdata/TestCoderAgent/zai-glm4.6/glob_tool.yaml                   |  20 
internal/agent/testdata/TestCoderAgent/zai-glm4.6/grep_tool.yaml                   |  22 
internal/agent/testdata/TestCoderAgent/zai-glm4.6/ls_tool.yaml                     |   3 
internal/agent/testdata/TestCoderAgent/zai-glm4.6/multiedit_tool.yaml              |  24 
internal/agent/testdata/TestCoderAgent/zai-glm4.6/parallel_tool_calls.yaml         |   2 
internal/agent/testdata/TestCoderAgent/zai-glm4.6/read_a_file.yaml                 |  16 
internal/agent/testdata/TestCoderAgent/zai-glm4.6/simple_test.yaml                 |  12 
internal/agent/testdata/TestCoderAgent/zai-glm4.6/sourcegraph_tool.yaml            |   3 
internal/agent/testdata/TestCoderAgent/zai-glm4.6/update_a_file.yaml               |  22 
internal/agent/testdata/TestCoderAgent/zai-glm4.6/write_tool.yaml                  |  20 
internal/agent/tools/fetch.go                                                      |  18 
internal/agent/tools/fetch.md                                                      |  19 
internal/agent/tools/fetch_helpers.go                                              |  96 
internal/agent/tools/fetch_types.go                                                |  41 
internal/agent/tools/mcp-tools.go                                                  | 471 
internal/agent/tools/mcp/init.go                                                   | 405 
internal/agent/tools/mcp/prompts.go                                                |  87 
internal/agent/tools/mcp/tools.go                                                  |  93 
internal/agent/tools/view.go                                                       |   7 
internal/agent/tools/web_fetch.go                                                  |  72 
internal/agent/tools/web_fetch.md                                                  |  28 
internal/app/app.go                                                                |  79 
internal/cmd/dirs.go                                                               |   4 
internal/cmd/root.go                                                               |  19 
internal/cmd/run.go                                                                |  19 
internal/cmd/update_providers.go                                                   |   2 
internal/config/config.go                                                          |   1 
internal/config/load.go                                                            |  11 
internal/config/load_test.go                                                       |   4 
internal/config/provider.go                                                        |  18 
internal/db/connect.go                                                             |   2 
internal/format/spinner.go                                                         |  25 
internal/home/home.go                                                              |  35 
internal/permission/permission.go                                                  |   2 
internal/term/term.go                                                              |  15 
internal/tui/components/anim/anim.go                                               |   4 
internal/tui/components/chat/chat.go                                               |   9 
internal/tui/components/chat/editor/editor.go                                      |  19 
internal/tui/components/chat/editor/keys.go                                        |   2 
internal/tui/components/chat/header/header.go                                      |   4 
internal/tui/components/chat/messages/messages.go                                  |  41 
internal/tui/components/chat/messages/renderer.go                                  | 192 
internal/tui/components/chat/messages/tool.go                                      |  79 
internal/tui/components/chat/queue.go                                              |   2 
internal/tui/components/chat/sidebar/sidebar.go                                    |   4 
internal/tui/components/chat/splash/keys.go                                        |   2 
internal/tui/components/chat/splash/splash.go                                      |   8 
internal/tui/components/completions/completions.go                                 |   6 
internal/tui/components/completions/keys.go                                        |   2 
internal/tui/components/core/core.go                                               |   6 
internal/tui/components/core/layout/layout.go                                      |   4 
internal/tui/components/core/status/status.go                                      |   6 
internal/tui/components/dialogs/commands/arguments.go                              | 172 
internal/tui/components/dialogs/commands/commands.go                               | 112 
internal/tui/components/dialogs/commands/keys.go                                   |   2 
internal/tui/components/dialogs/commands/loader.go                                 | 101 
internal/tui/components/dialogs/dialogs.go                                         |   4 
internal/tui/components/dialogs/filepicker/filepicker.go                           |  10 
internal/tui/components/dialogs/filepicker/keys.go                                 |   2 
internal/tui/components/dialogs/keys.go                                            |   2 
internal/tui/components/dialogs/models/apikey.go                                   |   8 
internal/tui/components/dialogs/models/keys.go                                     |   2 
internal/tui/components/dialogs/models/list.go                                     |   2 
internal/tui/components/dialogs/models/models.go                                   |  10 
internal/tui/components/dialogs/permissions/keys.go                                |   2 
internal/tui/components/dialogs/permissions/permissions.go                         |  31 
internal/tui/components/dialogs/quit/keys.go                                       |   2 
internal/tui/components/dialogs/quit/quit.go                                       |   6 
internal/tui/components/dialogs/reasoning/reasoning.go                             |   8 
internal/tui/components/dialogs/sessions/keys.go                                   |   2 
internal/tui/components/dialogs/sessions/sessions.go                               |   8 
internal/tui/components/files/files.go                                             |   2 
internal/tui/components/image/image.go                                             |   2 
internal/tui/components/image/load.go                                              |   2 
internal/tui/components/logo/logo.go                                               |   2 
internal/tui/components/lsp/lsp.go                                                 |   2 
internal/tui/components/mcp/mcp.go                                                 |  26 
internal/tui/exp/diffview/chroma.go                                                |   2 
internal/tui/exp/diffview/diffview.go                                              |   2 
internal/tui/exp/diffview/style.go                                                 |   2 
internal/tui/exp/list/filterable.go                                                |   8 
internal/tui/exp/list/filterable_group.go                                          |   8 
internal/tui/exp/list/grouped.go                                                   |   2 
internal/tui/exp/list/items.go                                                     |   4 
internal/tui/exp/list/keys.go                                                      |   2 
internal/tui/exp/list/list.go                                                      |   6 
internal/tui/exp/list/list_test.go                                                 |   4 
internal/tui/keys.go                                                               |   2 
internal/tui/page/chat/chat.go                                                     |  17 
internal/tui/page/chat/keys.go                                                     |   2 
internal/tui/styles/charmtone.go                                                   |   2 
internal/tui/styles/markdown.go                                                    | 185 
internal/tui/styles/theme.go                                                       |  12 
internal/tui/tui.go                                                                |  75 
internal/tui/util/util.go                                                          |   2 
internal/ui/common/interface.go                                                    |   2 
internal/ui/dialog/dialog.go                                                       |   6 
internal/ui/dialog/quit.go                                                         |   6 
internal/ui/logo/logo.go                                                           |   2 
internal/ui/model/chat.go                                                          |   4 
internal/ui/model/editor.go                                                        |   6 
internal/ui/model/keys.go                                                          |   2 
internal/ui/model/sidebar.go                                                       |   4 
internal/ui/model/ui.go                                                            |  10 
internal/ui/styles/styles.go                                                       |  12 
158 files changed, 2,970 insertions(+), 1,548 deletions(-)

Detailed changes

.github/cla-signatures.json 🔗

@@ -807,6 +807,22 @@
       "created_at": "2025-11-01T10:06:05Z",
       "repoId": 987670088,
       "pullRequestNo": 1358
+    },
+    {
+      "name": "LarsArtmann",
+      "id": 23587853,
+      "comment_id": 3488527230,
+      "created_at": "2025-11-05T00:18:02Z",
+      "repoId": 987670088,
+      "pullRequestNo": 1384
+    },
+    {
+      "name": "danielmerja",
+      "id": 30878766,
+      "comment_id": 3492618827,
+      "created_at": "2025-11-05T17:59:51Z",
+      "repoId": 987670088,
+      "pullRequestNo": 1387
     }
   ]
 }

CRUSH.md 🔗

@@ -60,6 +60,13 @@ func TestYourFunction(t *testing.T) {
   - You can also use `task fmt` to run `gofumpt -w .` on the entire project,
     as long as `gofumpt` is on the `PATH`.
 
+## Comments
+
+- Comments that live on their own lines should start with capital letters and
+  end with periods. Wrap comments at 78 columns.
+
 ## Committing
 
 - ALWAYS use semantic commits (`fix:`, `feat:`, `chore:`, `refactor:`, `docs:`, `sec:`, etc).
+- Try to keep commits to one line, not including your attribution. Only use
+  multi-line commits when additional context is truly necessary.

README.md 🔗

@@ -211,7 +211,7 @@ or globally, with the following priority:
 
 1. `.crush.json`
 2. `crush.json`
-3. `$HOME/.config/crush/crush.json` (Windows: `%USERPROFILE%\AppData\Local\crush\crush.json`)
+3. `$HOME/.config/crush/crush.json`
 
 Configuration itself is stored as a JSON object:
 
@@ -281,11 +281,11 @@ using `$(echo $VAR)` syntax.
     },
     "github": {
       "type": "http",
-      "url": "https://example.com/mcp/",
+      "url": "https://api.githubcopilot.com/mcp/",
       "timeout": 120,
       "disabled": false,
       "headers": {
-        "Authorization": "$(echo Bearer $EXAMPLE_MCP_TOKEN)"
+        "Authorization": "Bearer $GH_PAT"
       }
     },
     "streaming-service": {

go.mod 🔗

@@ -3,7 +3,10 @@ module github.com/charmbracelet/crush
 go 1.25.0
 
 require (
-	charm.land/fantasy v0.1.5
+	charm.land/bubbles/v2 v2.0.0-beta.1.0.20251104200223-da0b892d1759
+	charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251105182244-3138f1cd1bf8
+	charm.land/fantasy v0.1.6
+	charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251104200114-3aae28661422
 	github.com/JohannesKaufmann/html-to-markdown v1.6.0
 	github.com/MakeNowJust/heredoc v1.0.0
 	github.com/PuerkitoBio/goquery v1.10.3
@@ -12,18 +15,15 @@ require (
 	github.com/aymanbagabas/go-udiff v0.3.1
 	github.com/bmatcuk/doublestar/v4 v4.9.1
 	github.com/charlievieth/fastwalk v1.0.14
-	github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2
-	github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.6
 	github.com/charmbracelet/catwalk v0.8.2
-	github.com/charmbracelet/colorprofile v0.3.2
+	github.com/charmbracelet/colorprofile v0.3.3
 	github.com/charmbracelet/fang v0.4.3
 	github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018
-	github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea
 	github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706
-	github.com/charmbracelet/ultraviolet v0.0.0-20251017140847-d4ace4d6e731
-	github.com/charmbracelet/x/ansi v0.10.2
+	github.com/charmbracelet/ultraviolet v0.0.0-20251105181648-75d1e37ff1bb
+	github.com/charmbracelet/x/ansi v0.10.3
 	github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3
-	github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a
+	github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f
 	github.com/charmbracelet/x/exp/ordered v0.1.0
 	github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5
 	github.com/charmbracelet/x/powernap v0.0.0-20251015113943-25f979b54ad4
@@ -69,7 +69,7 @@ require (
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
 	github.com/andybalholm/cascadia v1.3.3 // indirect
-	github.com/aws/aws-sdk-go-v2 v1.39.4 // indirect
+	github.com/aws/aws-sdk-go-v2 v1.39.5 // indirect
 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
 	github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
 	github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
@@ -88,11 +88,14 @@ require (
 	github.com/bahlo/generic-list-go v0.2.0 // indirect
 	github.com/buger/jsonparser v1.1.1 // indirect
 	github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251024181547-21d6f3d9a904 // indirect
-	github.com/charmbracelet/go-genai v0.0.0-20251021165952-9befde14ce97 // indirect
+	github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20251103214348-d3032608aa74 // indirect
 	github.com/charmbracelet/x/cellbuf v0.0.14-0.20250811133356-e0c5dbe5ea4a // indirect
 	github.com/charmbracelet/x/json v0.2.0 // indirect
 	github.com/charmbracelet/x/termios v0.1.1 // indirect
 	github.com/charmbracelet/x/windows v0.2.2 // indirect
+	github.com/clipperhouse/displaywidth v0.4.1 // indirect
+	github.com/clipperhouse/stringish v0.1.1 // indirect
+	github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/disintegration/gift v1.1.2 // indirect
 	github.com/dlclark/regexp2 v1.11.5 // indirect
@@ -117,7 +120,7 @@ require (
 	github.com/klauspost/pgzip v1.2.6 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
-	github.com/mattn/go-runewidth v0.0.17 // indirect
+	github.com/mattn/go-runewidth v0.0.19 // indirect
 	github.com/mfridman/interpolate v0.0.2 // indirect
 	github.com/microcosm-cc/bluemonday v1.0.27 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -160,6 +163,7 @@ require (
 	golang.org/x/term v0.35.0 // indirect
 	golang.org/x/time v0.12.0 // indirect
 	google.golang.org/api v0.239.0 // indirect
+	google.golang.org/genai v1.33.1-0.20251103191629-d15baab4f79e // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	google.golang.org/grpc v1.74.2 // indirect
 	google.golang.org/protobuf v1.36.10 // indirect

go.sum 🔗

@@ -1,5 +1,11 @@
-charm.land/fantasy v0.1.5 h1:7sta5yC+cSU32Kb+cNQb4b/3fyn13tYOgXsnXhdMlX0=
-charm.land/fantasy v0.1.5/go.mod h1:GT1Y8uYNmmu7OkUxWEiOyzdAf1jYopPJfpWvoDRzGiM=
+charm.land/bubbles/v2 v2.0.0-beta.1.0.20251104200223-da0b892d1759 h1:P1MxkVl8ZeI9tHmmrn9UzV/5Mz7heoiTgqECHRFsUcs=
+charm.land/bubbles/v2 v2.0.0-beta.1.0.20251104200223-da0b892d1759/go.mod h1:G7JWaj3kDT0BDB+h5BLDUhhBLpDoRLKrpOp5QrA2SQs=
+charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251105182244-3138f1cd1bf8 h1:A1y0nyy7ykH1judtnD36sgpepLBrU4y7mN6QlZEVhZk=
+charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251105182244-3138f1cd1bf8/go.mod h1:oR2A+f83vzDY0hALwW4eh90fKXdranRWnH/vfwJL1lU=
+charm.land/fantasy v0.1.6 h1:laomMUqUaniQoLx7UOb+MLUpIGJPoNwsXvw1PbzgnB8=
+charm.land/fantasy v0.1.6/go.mod h1:JpFcJ5zs/1CjmYYGAZ7GaFmeBv0mPaTzEPRG6Eic5pc=
+charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251104200114-3aae28661422 h1:LcW3SSv1EZvlb9pfaVZIZyHrPVRJdb0adgX+tWPYl0k=
+charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251104200114-3aae28661422/go.mod h1:0EJAlA1PDGb+2RyyC02yDSPDwvpegDefu74HC9Blg5o=
 cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
 cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
 cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
@@ -34,8 +40,8 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk
 github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
 github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
 github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
-github.com/aws/aws-sdk-go-v2 v1.39.4 h1:qTsQKcdQPHnfGYBBs+Btl8QwxJeoWcOcPcixK90mRhg=
-github.com/aws/aws-sdk-go-v2 v1.39.4/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM=
+github.com/aws/aws-sdk-go-v2 v1.39.5 h1:e/SXuia3rkFtapghJROrydtQpfQaaUgd1cUvyO1mp2w=
+github.com/aws/aws-sdk-go-v2 v1.39.5/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
 github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
@@ -78,34 +84,28 @@ github.com/charlievieth/fastwalk v1.0.14 h1:3Eh5uaFGwHZd8EGwTjJnSpBkfwfsak9h6ICg
 github.com/charlievieth/fastwalk v1.0.14/go.mod h1:diVcUreiU1aQ4/Wu3NbxxH4/KYdKpLDojrQ1Bb2KgNY=
 github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251024181547-21d6f3d9a904 h1:rwLdEpG9wE6kL69KkEKDiWprO8pQOZHZXeod6+9K+mw=
 github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251024181547-21d6f3d9a904/go.mod h1:8TIYxZxsuCqqeJ0lga/b91tBwrbjoHDC66Sq5t8N2R4=
-github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2 h1:973OHYuq2Jx9deyuPwe/6lsuQrDCatOsjP8uCd02URE=
-github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
-github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.6 h1:nXNg4TmtfoQXFdF2BSSjTxFp9bSHQCILkIKK3FXMW/E=
-github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.6/go.mod h1:SUTLq+/pGQ5qntHgt0JswfVJFfgJgWDqyvyiSLVlmbo=
 github.com/charmbracelet/catwalk v0.8.2 h1:J7xq/ft/ZByJCHl3JpgvxlCd59bzZPugy66XuoL4vAs=
 github.com/charmbracelet/catwalk v0.8.2/go.mod h1:ReU4SdrLfe63jkEjWMdX2wlZMV3k9r11oQAmzN0m+KY=
-github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
-github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
+github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
+github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
 github.com/charmbracelet/fang v0.4.3 h1:qXeMxnL4H6mSKBUhDefHu8NfikFbP/MBNTfqTrXvzmY=
 github.com/charmbracelet/fang v0.4.3/go.mod h1:wHJKQYO5ReYsxx+yZl+skDtrlKO/4LLEQ6EXsdHhRhg=
 github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018 h1:PU4Zvpagsk5sgaDxn5W4sxHuLp9QRMBZB3bFSk40A4w=
 github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018/go.mod h1:Z/GLmp9fzaqX4ze3nXG7StgWez5uBM5XtlLHK8V/qSk=
-github.com/charmbracelet/go-genai v0.0.0-20251021165952-9befde14ce97 h1:HK7B5Q+0FidxjQD5CovniMw7axkUeMHwgVkxkbmiW/s=
-github.com/charmbracelet/go-genai v0.0.0-20251021165952-9befde14ce97/go.mod h1:ZagL2esO4qxlOJBj0d4PVvLM82akQFtne8s3ivxBnTQ=
-github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea h1:g1HfUgSMvye8mgecMD1mPscpt+pzJoDEiSA+p2QXzdQ=
-github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea/go.mod h1:ngHerf1JLJXBrDXdphn5gFrBPriCL437uwukd5c93pM=
+github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20251103214348-d3032608aa74 h1:2N+CxpUFM6Rrx+xT7XaqM9pp/psOFlxKWa5R7rP/lck=
+github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20251103214348-d3032608aa74/go.mod h1:RfXmCdNs2F4MVJjBVQp5RZYXR05MiRAHN4GHwWmsNIA=
 github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE=
 github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM=
-github.com/charmbracelet/ultraviolet v0.0.0-20251017140847-d4ace4d6e731 h1:Lr+igmzKpLPdb8yUZBP9noYWwCZP042z2nWPrJZTc+8=
-github.com/charmbracelet/ultraviolet v0.0.0-20251017140847-d4ace4d6e731/go.mod h1:KfWwUa0Oe//D72YlhbOq/g40L7UiGtATrvsGI3cciG8=
-github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw=
-github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8=
+github.com/charmbracelet/ultraviolet v0.0.0-20251105181648-75d1e37ff1bb h1:KZnKSrGjarKScpekDuPAVnlMSMtA7mdzmoUD0AhAZC0=
+github.com/charmbracelet/ultraviolet v0.0.0-20251105181648-75d1e37ff1bb/go.mod h1:G7cNuWgmuugx6ApJv4kDGfnFanoDAz8AWazH9lSoWdw=
+github.com/charmbracelet/x/ansi v0.10.3 h1:3WoV9XN8uMEnFRZZ+vBPRy59TaIWa+gJodS4Vg5Fut0=
+github.com/charmbracelet/x/ansi v0.10.3/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE=
 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250811133356-e0c5dbe5ea4a h1:zYSNtEJM9jwHbJts2k+Hroj+xQwsW1yxc4Wopdv7KaI=
 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250811133356-e0c5dbe5ea4a/go.mod h1:rc2bsPC6MWae3LdOxNO1mOb443NlMrrDL0xEya48NNc=
 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3 h1:1xwHZg6eMZ9Wv5TE1UGub6ARubyOd1Lo5kPUI/6VL50=
 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0=
-github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw=
-github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
+github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
+github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
 github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
 github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
 github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5 h1:DTSZxdV9qQagD4iGcAt9RgaRBZtJl01bfKgdLzUzUPI=
@@ -120,6 +120,12 @@ github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8
 github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
 github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
 github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
+github.com/clipperhouse/displaywidth v0.4.1 h1:uVw9V8UDfnggg3K2U84VWY1YLQ/x2aKSCtkRyYozfoU=
+github.com/clipperhouse/displaywidth v0.4.1/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
+github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
+github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
+github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
+github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
 github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
@@ -206,8 +212,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ=
-github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
+github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
 github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
 github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
 github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
@@ -255,7 +261,6 @@ github.com/qjebbs/go-jsons v1.0.0-alpha.4 h1:Qsb4ohRUHQODIUAsJKdKJ/SIDbsO7oGOzsf
 github.com/qjebbs/go-jsons v1.0.0-alpha.4/go.mod h1:wNJrtinHyC3YSf6giEh4FJN8+yZV7nXBjvmfjhBIcw4=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
-github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -437,6 +442,8 @@ golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo=
 google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
+google.golang.org/genai v1.33.1-0.20251103191629-d15baab4f79e h1:pGBT6ptC4ENtN9wA4dGhvjwrYpVZ6X9Lnpwu4Y+jozk=
+google.golang.org/genai v1.33.1-0.20251103191629-d15baab4f79e/go.mod h1:7pAilaICJlQBonjKKJNhftDFv3SREhZcTe9F6nRcjbg=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=

internal/agent/agent.go 🔗

@@ -1,3 +1,10 @@
+// Package agent is the core orchestration layer for Crush AI agents.
+//
+// It provides session-based AI agent functionality for managing
+// conversations, tool execution, and message handling. It coordinates
+// interactions between language models, messages, sessions, and tools while
+// handling features like automatic summarization, queuing, and token
+// management.
 package agent
 
 import (
@@ -131,7 +138,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 	}
 
 	if len(a.tools) > 0 {
-		// add anthropic caching to the last tool
+		// Add Anthropic caching to the last tool.
 		a.tools[len(a.tools)-1].SetProviderOptions(a.getCacheControlOptions())
 	}
 
@@ -153,7 +160,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 	}
 
 	var wg sync.WaitGroup
-	// Generate title if first message
+	// Generate title if first message.
 	if len(msgs) == 0 {
 		wg.Go(func() {
 			sessionLock.Lock()
@@ -162,13 +169,13 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 		})
 	}
 
-	// Add the user message to the session
+	// Add the user message to the session.
 	_, err = a.createUserMessage(ctx, call)
 	if err != nil {
 		return nil, err
 	}
 
-	// add the session to the context
+	// Add the session to the context.
 	ctx = context.WithValue(ctx, tools.SessionIDContextKey, call.SessionID)
 
 	genCtx, cancel := context.WithCancel(ctx)
@@ -195,10 +202,10 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 		PresencePenalty:  call.PresencePenalty,
 		TopK:             call.TopK,
 		FrequencyPenalty: call.FrequencyPenalty,
-		// Before each step create the new assistant message
+		// Before each step create a new assistant message.
 		PrepareStep: func(callContext context.Context, options fantasy.PrepareStepFunctionOptions) (_ context.Context, prepared fantasy.PrepareStepResult, err error) {
 			prepared.Messages = options.Messages
-			// reset all cached items
+			// Reset all cached items.
 			for i := range prepared.Messages {
 				prepared.Messages[i].ProviderOptions = nil
 			}
@@ -216,14 +223,14 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 			lastSystemRoleInx := 0
 			systemMessageUpdated := false
 			for i, msg := range prepared.Messages {
-				// only add cache control to the last message
+				// Only add cache control to the last message.
 				if msg.Role == fantasy.MessageRoleSystem {
 					lastSystemRoleInx = i
 				} else if !systemMessageUpdated {
 					prepared.Messages[lastSystemRoleInx].ProviderOptions = a.getCacheControlOptions()
 					systemMessageUpdated = true
 				}
-				// than add cache control to the last 2 messages
+				// Than add cache control to the last 2 messages.
 				if i > len(prepared.Messages)-3 {
 					prepared.Messages[i].ProviderOptions = a.getCacheControlOptions()
 				}
@@ -276,6 +283,13 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 			return a.messages.Update(genCtx, *currentAssistant)
 		},
 		OnTextDelta: func(id string, text string) error {
+			// Strip leading newline from initial text content. This is is
+			// particularly important in non-interactive mode where leading
+			// newlines are very visible.
+			if len(currentAssistant.Parts) == 0 {
+				text = strings.TrimPrefix(text, "\n")
+			}
+
 			currentAssistant.AppendContent(text)
 			return a.messages.Update(genCtx, *currentAssistant)
 		},
@@ -387,10 +401,10 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 		if currentAssistant == nil {
 			return result, err
 		}
-		// Ensure we finish thinking on error to close the reasoning state
+		// Ensure we finish thinking on error to close the reasoning state.
 		currentAssistant.FinishThinking()
 		toolCalls := currentAssistant.ToolCalls()
-		// INFO: we use the parent context here because the genCtx has been cancelled
+		// INFO: we use the parent context here because the genCtx has been cancelled.
 		msgs, createErr := a.messages.List(ctx, currentAssistant.SessionID)
 		if createErr != nil {
 			return nil, createErr
@@ -427,7 +441,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 			if isCancelErr {
 				content = "Tool execution canceled by user"
 			} else if isPermissionErr {
-				content = "Permission denied"
+				content = "User denied permission"
 			}
 			toolResult := message.ToolResult{
 				ToolCallID: tc.ID,
@@ -446,13 +460,14 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 			}
 		}
 		if isCancelErr {
-			currentAssistant.AddFinish(message.FinishReasonCanceled, "Request cancelled", "")
+			currentAssistant.AddFinish(message.FinishReasonCanceled, "User canceled request", "")
 		} else if isPermissionErr {
-			currentAssistant.AddFinish(message.FinishReasonPermissionDenied, "Permission denied", "")
+			currentAssistant.AddFinish(message.FinishReasonPermissionDenied, "User denied permission", "")
 		} else {
 			currentAssistant.AddFinish(message.FinishReasonError, "API Error", err.Error())
 		}
-		// INFO: we use the parent context here because the genCtx has been cancelled
+		// Note: we use the parent context here because the genCtx has been
+		// cancelled.
 		updateErr := a.messages.Update(ctx, *currentAssistant)
 		if updateErr != nil {
 			return nil, updateErr
@@ -466,7 +481,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 		if summarizeErr := a.Summarize(genCtx, call.SessionID, call.ProviderOptions); summarizeErr != nil {
 			return nil, summarizeErr
 		}
-		// if the agent was not done...
+		// If the agent wasn't done...
 		if len(currentAssistant.ToolCalls()) > 0 {
 			existing, ok := a.messageQueue.Get(call.SessionID)
 			if !ok {
@@ -478,7 +493,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 		}
 	}
 
-	// release active request before processing queued messages
+	// Release active request before processing queued messages.
 	a.activeRequests.Del(call.SessionID)
 	cancel()
 
@@ -486,7 +501,7 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 	if !ok || len(queuedMessages) == 0 {
 		return result, err
 	}
-	// there are queued messages restart the loop
+	// There are queued messages restart the loop.
 	firstQueuedMessage := queuedMessages[0]
 	a.messageQueue.Set(call.SessionID, queuedMessages[1:])
 	return a.Run(ctx, firstQueuedMessage)
@@ -506,7 +521,7 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string, opts fan
 		return err
 	}
 	if len(msgs) == 0 {
-		// nothing to summarize
+		// Nothing to summarize.
 		return nil
 	}
 
@@ -546,7 +561,7 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string, opts fan
 			return a.messages.Update(genCtx, summaryMessage)
 		},
 		OnReasoningEnd: func(id string, reasoning fantasy.ReasoningContent) error {
-			// handle anthropic signature
+			// Handle anthropic signature.
 			if anthropicData, ok := reasoning.ProviderMetadata["anthropic"]; ok {
 				if signature, ok := anthropicData.(*anthropic.ReasoningOptionMetadata); ok && signature.Signature != "" {
 					summaryMessage.AppendReasoningSignature(signature.Signature)
@@ -563,7 +578,7 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string, opts fan
 	if err != nil {
 		isCancelErr := errors.Is(err, context.Canceled)
 		if isCancelErr {
-			// User cancelled summarize we need to remove the summary message
+			// User cancelled summarize we need to remove the summary message.
 			deleteErr := a.messages.Delete(ctx, summaryMessage.ID)
 			return deleteErr
 		}
@@ -590,7 +605,7 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string, opts fan
 
 	a.updateSessionUsage(a.largeModel, &currentSession, resp.TotalUsage, openrouterCost)
 
-	// just in case get just the last usage
+	// Just in case, get just the last usage info.
 	usage := resp.Response.Usage
 	currentSession.SummaryMessageID = summaryMessage.ID
 	currentSession.CompletionTokens = usage.OutputTokens
@@ -636,7 +651,8 @@ func (a *sessionAgent) preparePrompt(msgs []message.Message, attachments ...mess
 		if len(m.Parts) == 0 {
 			continue
 		}
-		// Assistant message without content or tool calls (cancelled before it returned anything)
+		// Assistant message without content or tool calls (cancelled before it
+		// returned anything).
 		if m.Role == message.Assistant && len(m.ToolCalls()) == 0 && m.Content().Text == "" && m.ReasoningContent().String() == "" {
 			continue
 		}
@@ -711,7 +727,7 @@ func (a *sessionAgent) generateTitle(ctx context.Context, session *session.Sessi
 
 	title = strings.ReplaceAll(title, "\n", " ")
 
-	// remove thinking tags if present
+	// Remove thinking tags if present.
 	if idx := strings.Index(title, "</think>"); idx > 0 {
 		title = title[idx+len("</think>"):]
 	}
@@ -777,13 +793,13 @@ func (a *sessionAgent) updateSessionUsage(model Model, session *session.Session,
 }
 
 func (a *sessionAgent) Cancel(sessionID string) {
-	// Cancel regular requests
+	// Cancel regular requests.
 	if cancel, ok := a.activeRequests.Take(sessionID); ok && cancel != nil {
 		slog.Info("Request cancellation initiated", "session_id", sessionID)
 		cancel()
 	}
 
-	// Also check for summarize requests
+	// Also check for summarize requests.
 	if cancel, ok := a.activeRequests.Take(sessionID + "-summarize"); ok && cancel != nil {
 		slog.Info("Summarize cancellation initiated", "session_id", sessionID)
 		cancel()

internal/agent/agent_tool.go 🔗

@@ -3,7 +3,6 @@ package agent
 import (
 	"context"
 	_ "embed"
-	"encoding/json"
 	"errors"
 	"fmt"
 
@@ -43,9 +42,6 @@ func (c *coordinator) agentTool(ctx context.Context) (fantasy.AgentTool, error)
 		AgentToolName,
 		string(agentToolDescription),
 		func(ctx context.Context, params AgentParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
-			if err := json.Unmarshal([]byte(call.Input), &params); err != nil {
-				return fantasy.NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
-			}
 			if params.Prompt == "" {
 				return fantasy.NewTextErrorResponse("prompt is required"), nil
 			}

internal/agent/agentic_fetch_tool.go 🔗

@@ -0,0 +1,217 @@
+package agent
+
+import (
+	"context"
+	_ "embed"
+	"errors"
+	"fmt"
+	"net/http"
+	"os"
+	"time"
+
+	"charm.land/fantasy"
+
+	"github.com/charmbracelet/crush/internal/agent/prompt"
+	"github.com/charmbracelet/crush/internal/agent/tools"
+	"github.com/charmbracelet/crush/internal/permission"
+)
+
+//go:embed templates/agentic_fetch.md
+var agenticFetchToolDescription []byte
+
+// agenticFetchValidationResult holds the validated parameters from the tool call context.
+type agenticFetchValidationResult struct {
+	SessionID      string
+	AgentMessageID string
+}
+
+// validateAgenticFetchParams validates the tool call parameters and extracts required context values.
+func validateAgenticFetchParams(ctx context.Context, params tools.AgenticFetchParams) (agenticFetchValidationResult, error) {
+	if params.URL == "" {
+		return agenticFetchValidationResult{}, errors.New("url is required")
+	}
+
+	if params.Prompt == "" {
+		return agenticFetchValidationResult{}, errors.New("prompt is required")
+	}
+
+	sessionID := tools.GetSessionFromContext(ctx)
+	if sessionID == "" {
+		return agenticFetchValidationResult{}, errors.New("session id missing from context")
+	}
+
+	agentMessageID := tools.GetMessageFromContext(ctx)
+	if agentMessageID == "" {
+		return agenticFetchValidationResult{}, errors.New("agent message id missing from context")
+	}
+
+	return agenticFetchValidationResult{
+		SessionID:      sessionID,
+		AgentMessageID: agentMessageID,
+	}, nil
+}
+
+//go:embed templates/agentic_fetch_prompt.md.tpl
+var agenticFetchPromptTmpl []byte
+
+func (c *coordinator) agenticFetchTool(_ context.Context, client *http.Client) (fantasy.AgentTool, error) {
+	if client == nil {
+		client = &http.Client{
+			Timeout: 30 * time.Second,
+			Transport: &http.Transport{
+				MaxIdleConns:        100,
+				MaxIdleConnsPerHost: 10,
+				IdleConnTimeout:     90 * time.Second,
+			},
+		}
+	}
+
+	return fantasy.NewAgentTool(
+		tools.AgenticFetchToolName,
+		string(agenticFetchToolDescription),
+		func(ctx context.Context, params tools.AgenticFetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
+			validationResult, err := validateAgenticFetchParams(ctx, params)
+			if err != nil {
+				return fantasy.NewTextErrorResponse(err.Error()), nil
+			}
+
+			p := c.permissions.Request(
+				permission.CreatePermissionRequest{
+					SessionID:   validationResult.SessionID,
+					Path:        c.cfg.WorkingDir(),
+					ToolCallID:  call.ID,
+					ToolName:    tools.AgenticFetchToolName,
+					Action:      "fetch",
+					Description: fmt.Sprintf("Fetch and analyze content from URL: %s", params.URL),
+					Params:      tools.AgenticFetchPermissionsParams(params),
+				},
+			)
+
+			if !p {
+				return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
+			}
+
+			content, err := tools.FetchURLAndConvert(ctx, client, params.URL)
+			if err != nil {
+				return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to fetch URL: %s", err)), nil
+			}
+
+			tmpDir, err := os.MkdirTemp(c.cfg.Options.DataDirectory, "crush-fetch-*")
+			if err != nil {
+				return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to create temporary directory: %s", err)), nil
+			}
+			defer os.RemoveAll(tmpDir)
+
+			hasLargeContent := len(content) > tools.LargeContentThreshold
+			var fullPrompt string
+
+			if hasLargeContent {
+				tempFile, err := os.CreateTemp(tmpDir, "page-*.md")
+				if err != nil {
+					return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to create temporary file: %s", err)), nil
+				}
+				tempFilePath := tempFile.Name()
+
+				if _, err := tempFile.WriteString(content); err != nil {
+					tempFile.Close()
+					return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to write content to file: %s", err)), nil
+				}
+				tempFile.Close()
+
+				fullPrompt = fmt.Sprintf("%s\n\nThe web page from %s has been saved to: %s\n\nUse the view and grep tools to analyze this file and extract the requested information.", params.Prompt, params.URL, tempFilePath)
+			} else {
+				fullPrompt = fmt.Sprintf("%s\n\nWeb page URL: %s\n\n<webpage_content>\n%s\n</webpage_content>", params.Prompt, params.URL, content)
+			}
+
+			promptOpts := []prompt.Option{
+				prompt.WithWorkingDir(tmpDir),
+			}
+
+			promptTemplate, err := prompt.NewPrompt("agentic_fetch", string(agenticFetchPromptTmpl), promptOpts...)
+			if err != nil {
+				return fantasy.ToolResponse{}, fmt.Errorf("error creating prompt: %s", err)
+			}
+
+			_, small, err := c.buildAgentModels(ctx)
+			if err != nil {
+				return fantasy.ToolResponse{}, fmt.Errorf("error building models: %s", err)
+			}
+
+			systemPrompt, err := promptTemplate.Build(ctx, small.Model.Provider(), small.Model.Model(), *c.cfg)
+			if err != nil {
+				return fantasy.ToolResponse{}, fmt.Errorf("error building system prompt: %s", err)
+			}
+
+			smallProviderCfg, ok := c.cfg.Providers.Get(small.ModelCfg.Provider)
+			if !ok {
+				return fantasy.ToolResponse{}, errors.New("small model provider not configured")
+			}
+
+			webFetchTool := tools.NewWebFetchTool(tmpDir, client)
+			fetchTools := []fantasy.AgentTool{
+				webFetchTool,
+				tools.NewGlobTool(tmpDir),
+				tools.NewGrepTool(tmpDir),
+				tools.NewViewTool(c.lspClients, c.permissions, tmpDir),
+			}
+
+			agent := NewSessionAgent(SessionAgentOptions{
+				LargeModel:           small, // Use small model for both (fetch doesn't need large)
+				SmallModel:           small,
+				SystemPromptPrefix:   smallProviderCfg.SystemPromptPrefix,
+				SystemPrompt:         systemPrompt,
+				DisableAutoSummarize: c.cfg.Options.DisableAutoSummarize,
+				IsYolo:               c.permissions.SkipRequests(),
+				Sessions:             c.sessions,
+				Messages:             c.messages,
+				Tools:                fetchTools,
+			})
+
+			agentToolSessionID := c.sessions.CreateAgentToolSessionID(validationResult.AgentMessageID, call.ID)
+			session, err := c.sessions.CreateTaskSession(ctx, agentToolSessionID, validationResult.SessionID, "Fetch Analysis")
+			if err != nil {
+				return fantasy.ToolResponse{}, fmt.Errorf("error creating session: %s", err)
+			}
+
+			c.permissions.AutoApproveSession(session.ID)
+
+			// Use small model for web content analysis (faster and cheaper)
+			maxTokens := small.CatwalkCfg.DefaultMaxTokens
+			if small.ModelCfg.MaxTokens != 0 {
+				maxTokens = small.ModelCfg.MaxTokens
+			}
+
+			result, err := agent.Run(ctx, SessionAgentCall{
+				SessionID:        session.ID,
+				Prompt:           fullPrompt,
+				MaxOutputTokens:  maxTokens,
+				ProviderOptions:  getProviderOptions(small, smallProviderCfg),
+				Temperature:      small.ModelCfg.Temperature,
+				TopP:             small.ModelCfg.TopP,
+				TopK:             small.ModelCfg.TopK,
+				FrequencyPenalty: small.ModelCfg.FrequencyPenalty,
+				PresencePenalty:  small.ModelCfg.PresencePenalty,
+			})
+			if err != nil {
+				return fantasy.NewTextErrorResponse("error generating response"), nil
+			}
+
+			updatedSession, err := c.sessions.Get(ctx, session.ID)
+			if err != nil {
+				return fantasy.ToolResponse{}, fmt.Errorf("error getting session: %s", err)
+			}
+			parentSession, err := c.sessions.Get(ctx, validationResult.SessionID)
+			if err != nil {
+				return fantasy.ToolResponse{}, fmt.Errorf("error getting parent session: %s", err)
+			}
+
+			parentSession.Cost += updatedSession.Cost
+
+			_, err = c.sessions.Save(ctx, parentSession)
+			if err != nil {
+				return fantasy.ToolResponse{}, fmt.Errorf("error saving parent session: %s", err)
+			}
+
+			return fantasy.NewTextResponse(result.Response.Content.Text()), nil
+		}), nil
+}

internal/agent/coordinator.go 🔗

@@ -319,6 +319,14 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fan
 		allTools = append(allTools, agentTool)
 	}
 
+	if slices.Contains(agent.AllowedTools, tools.AgenticFetchToolName) {
+		agenticFetchTool, err := c.agenticFetchTool(ctx, nil)
+		if err != nil {
+			return nil, err
+		}
+		allTools = append(allTools, agenticFetchTool)
+	}
+
 	allTools = append(allTools,
 		tools.NewBashTool(c.permissions, c.cfg.WorkingDir(), c.cfg.Options.Attribution),
 		tools.NewDownloadTool(c.permissions, c.cfg.WorkingDir(), nil),
@@ -344,28 +352,24 @@ func (c *coordinator) buildTools(ctx context.Context, agent config.Agent) ([]fan
 		}
 	}
 
-	mcpTools := tools.GetMCPTools(context.Background(), c.permissions, c.cfg)
-
-	for _, mcpTool := range mcpTools {
+	for _, tool := range tools.GetMCPTools(c.permissions, c.cfg.WorkingDir()) {
 		if agent.AllowedMCP == nil {
 			// No MCP restrictions
-			filteredTools = append(filteredTools, mcpTool)
-		} else if len(agent.AllowedMCP) == 0 {
-			// no mcps allowed
+			filteredTools = append(filteredTools, tool)
+			continue
+		}
+		if len(agent.AllowedMCP) == 0 {
+			// No MCPs allowed
+			slog.Warn("MCPs not allowed")
 			break
 		}
 
 		for mcp, tools := range agent.AllowedMCP {
-			if mcp == mcpTool.MCP() {
-				if len(tools) == 0 {
-					filteredTools = append(filteredTools, mcpTool)
-				}
-				for _, t := range tools {
-					if t == mcpTool.MCPToolName() {
-						filteredTools = append(filteredTools, mcpTool)
-					}
-				}
-				break
+			if mcp != tool.MCP() {
+				continue
+			}
+			if len(tools) == 0 || slices.Contains(tools, tool.MCPToolName()) {
+				filteredTools = append(filteredTools, tool)
 			}
 		}
 	}
@@ -658,7 +662,6 @@ func (c *coordinator) buildProvider(providerCfg config.ProviderConfig, model con
 		}
 	}
 
-	// TODO: make sure we have
 	apiKey, _ := c.cfg.Resolve(providerCfg.APIKey)
 	baseURL, _ := c.cfg.Resolve(providerCfg.BaseURL)
 

internal/agent/templates/agentic_fetch.md 🔗

@@ -0,0 +1,51 @@
+Fetches content from a specified URL and processes it using an AI model to extract information or answer questions.
+
+<when_to_use>
+Use this tool when you need to:
+- Extract specific information from a webpage (e.g., "get pricing info")
+- Answer questions about web content (e.g., "what does this article say about X?")
+- Summarize or analyze web pages
+- Find specific data within large pages
+- Interpret or process web content with AI
+
+DO NOT use this tool when:
+- You just need raw content without analysis (use fetch instead - faster and cheaper)
+- You want direct access to API responses or JSON (use fetch instead)
+- You don't need the content processed or interpreted (use fetch instead)
+</when_to_use>
+
+<usage>
+- Takes a URL and a prompt as input
+- Fetches the URL content, converts HTML to markdown
+- Processes the content with the prompt using a small, fast model
+- Returns the model's response about the content
+- Use this tool when you need to retrieve and analyze web content
+</usage>
+
+<usage_notes>
+
+- IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions. All MCP-provided tools start with "mcp_".
+- The URL must be a fully-formed valid URL
+- HTTP URLs will be automatically upgraded to HTTPS
+- The prompt should describe what information you want to extract from the page
+- This tool is read-only and does not modify any files
+- Results will be summarized if the content is very large
+- For very large pages, the content will be saved to a temporary file and the agent will have access to grep/view tools to analyze it
+- When a URL redirects to a different host, the tool will inform you and provide the redirect URL. You should then make a new fetch request with the redirect URL to fetch the content.
+- This tool uses AI processing and costs more tokens than the simple fetch tool
+  </usage_notes>
+
+<limitations>
+- Max response size: 5MB
+- Only supports HTTP and HTTPS protocols
+- Cannot handle authentication or cookies
+- Some websites may block automated requests
+- Uses additional tokens for AI processing
+</limitations>
+
+<tips>
+- Be specific in your prompt about what information you want to extract
+- For complex pages, ask the agent to focus on specific sections
+- The agent has access to grep and view tools when analyzing large pages
+- If you just need raw content, use the fetch tool instead to save tokens
+</tips>

internal/agent/templates/agentic_fetch_prompt.md.tpl 🔗

@@ -0,0 +1,45 @@
+You are a web content analysis agent for Crush. Your task is to analyze web page content and extract the information requested by the user.
+
+<rules>
+1. You should be concise and direct in your responses
+2. Focus only on the information requested in the user's prompt
+3. If the content is provided in a file path, use the grep and view tools to efficiently search through it
+4. When relevant, quote specific sections from the page to support your answer
+5. If the requested information is not found, clearly state that
+6. Any file paths you use MUST be absolute
+7. **IMPORTANT**: If you need information from a linked page to answer the question, use the web_fetch tool to follow that link
+8. After fetching a link, analyze the content yourself to extract what's needed
+9. Don't hesitate to follow multiple links if necessary to get complete information
+10. **CRITICAL**: At the end of your response, include a "Sources" section listing ALL URLs that were useful in answering the question
+</rules>
+
+<response_format>
+Your response should be structured as follows:
+
+[Your answer to the user's question]
+
+## Sources
+- [URL 1 that was useful]
+- [URL 2 that was useful]
+- [URL 3 that was useful]
+...
+
+Only include URLs that actually contributed information to your answer. The main URL is always included. Add any additional URLs you fetched that provided relevant information.
+</response_format>
+
+<env>
+Working directory: {{.WorkingDir}}
+Platform: {{.Platform}}
+Today's date: {{.Date}}
+</env>
+
+<web_fetch_tool>
+You have access to a web_fetch tool that allows you to fetch additional web pages:
+- Use it when you need to follow links from the current page
+- Provide just the URL (no prompt parameter)
+- The tool will fetch and return the content (or save to a file if large)
+- YOU must then analyze that content to answer the user's question
+- **Use this liberally** - if a link seems relevant to answering the question, fetch it!
+- You can fetch multiple pages in sequence to gather all needed information
+- Remember to include any fetched URLs in your Sources section if they were helpful
+</web_fetch_tool>

internal/agent/testdata/TestCoderAgent/anthropic-sonnet/bash_tool.yaml 🔗

@@ -25,55 +25,52 @@ interactions:
     content_length: -1
     body: |+
       event: message_start
-      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_01VitTYtpvnwj5N6fBKySom4","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":152,"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":{"model":"claude-3-5-haiku-20241022","id":"msg_01GbNg6Ry9zebpJZ4SxbcBWA","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":152,"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":"Bash"}         }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Creating"}               }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" File"}               }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" a bash"}            }
 
       event: ping
       data: {"type": "ping"}
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Creation"}    }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" file"}            }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" with"}         }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" with hello"}      }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Hello"}          }
-
-      event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Message"}           }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" message"}           }
 
       event: content_block_stop
-      data: {"type":"content_block_stop","index":0  }
+      data: {"type":"content_block_stop","index":0        }
 
       event: message_delta
-      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":152,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10}          }
+      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":152,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10} }
 
       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: 560.092417ms
+    duration: 1.497832667s
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 45989
+    content_length: 46761
     host: ""

internal/agent/testdata/TestCoderAgent/anthropic-sonnet/download_tool.yaml 🔗

@@ -25,52 +25,52 @@ interactions:
     content_length: -1
     body: |+
       event: message_start
-      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_01SMGNsdc3MdjnDTj98MeSvC","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":160,"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"}}      }
+      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_01UXxqRskCGYn8pLqqgaFbfK","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":160,"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":"Downloa"}            }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Downloa"}              }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d Text"}       }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d File"}               }
 
       event: ping
       data: {"type": "ping"}
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" File from"}}
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" from"}            }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Example"}        }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Example-"}         }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" URL"}}
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Files Website"}     }
 
       event: content_block_stop
-      data: {"type":"content_block_stop","index":0  }
+      data: {"type":"content_block_stop","index":0    }
 
       event: message_delta
-      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":160,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":9}            }
+      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":160,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10}           }
 
       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: 506.850208ms
+    duration: 1.152031084s
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 46014
+    content_length: 46786
     host: ""

internal/agent/testdata/TestCoderAgent/anthropic-sonnet/fetch_tool.yaml 🔗

@@ -25,55 +25,52 @@ interactions:
     content_length: -1
     body: |+
       event: message_start
-      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_018xCr1DYiCf3jArVxFU59KR","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":167,"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"}}       }
+      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_01QBUSTN71eh95F8NGHNVJ7r","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":167,"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":"Web"}               }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Web"}       }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Page"}  }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Page Content Search"}         }
 
       event: ping
       data: {"type": "ping"}
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Search"}       }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" for"}           }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" for"}        }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" John"}           }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" John"}  }
-
-      event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Doe"}               }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Doe"}     }
 
       event: content_block_stop
-      data: {"type":"content_block_stop","index":0               }
+      data: {"type":"content_block_stop","index":0    }
 
       event: message_delta
-      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":167,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10} }
+      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":167,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":11}   }
 
       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: 601.795959ms
+    duration: 840.714166ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 46032
+    content_length: 46804
     host: ""

internal/agent/testdata/TestCoderAgent/anthropic-sonnet/glob_tool.yaml 🔗

@@ -25,49 +25,52 @@ interactions:
     content_length: -1
     body: |+
       event: message_start
-      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_019rtcSpEMDvDZB1L5jzASu7","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":142,"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"}}    }
+      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_014RSxCeV1UGnPSQkNpEQTUq","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":142,"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":"Fin"}        }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Fin"}}
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d Go"}             }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d Go"}         }
 
       event: ping
       data: {"type": "ping"}
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Files"}           }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Files"}         }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" in Current Directory"}          }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" with"}}
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Glob"}    }
 
       event: content_block_stop
-      data: {"type":"content_block_stop","index":0       }
+      data: {"type":"content_block_stop","index":0}
 
       event: message_delta
-      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":142,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":9}}
+      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":142,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":9}             }
 
       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: 827.522292ms
+    duration: 546.158083ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 45950
+    content_length: 46722
     host: ""

internal/agent/testdata/TestCoderAgent/anthropic-sonnet/grep_tool.yaml 🔗

@@ -25,31 +25,34 @@ interactions:
     content_length: -1
     body: |+
       event: message_start
-      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_01RAcxzeTopaX9juzPGBzfRV","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":144,"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":{"model":"claude-3-5-haiku-20241022","id":"msg_01MMg192G5uVZkAaHV6NCB7T","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":144,"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"}}  }
 
       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":"Grep"}    }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Searching"}        }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Package"}          }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Packages"}   }
 
       event: ping
       data: {"type": "ping"}
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Search"}            }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" in Go Files"}     }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" in Go Files"}            }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" with"}          }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Grep"}         }
 
       event: content_block_stop
-      data: {"type":"content_block_stop","index":0      }
+      data: {"type":"content_block_stop","index":0              }
 
       event: message_delta
-      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":144,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10} }
+      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":144,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":13}    }
 
       event: message_stop
       data: {"type":"message_stop"              }
@@ -59,15 +62,15 @@ interactions:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 551.294833ms
+    duration: 781.230459ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 45948
+    content_length: 46720
     host: ""

internal/agent/testdata/TestCoderAgent/anthropic-sonnet/ls_tool.yaml 🔗

@@ -25,31 +25,31 @@ interactions:
     content_length: -1
     body: |+
       event: message_start
-      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_016Uu4PnZb2k4Sx33HrinmEr","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":140,"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"}}        }
+      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_01B7Qa35Q32jAQNEVkyd7y87","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":140,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"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":"List"}   }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Listing Files"}           }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Files"}               }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" in Current Directory"}       }
 
       event: ping
       data: {"type": "ping"}
 
-      event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" in"}}
+      event: content_block_stop
+      data: {"type":"content_block_stop","index":0         }
 
-      event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Current Directory"}   }
+      event: ping
+      data: {"type": "ping"}
 
-      event: content_block_stop
-      data: {"type":"content_block_stop","index":0           }
+      event: ping
+      data: {"type": "ping"}
 
       event: message_delta
-      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":140,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":8}    }
+      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":140,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":9}   }
 
       event: message_stop
       data: {"type":"message_stop"            }
@@ -59,15 +59,15 @@ interactions:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 988.34175ms
+    duration: 750.596875ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 45942
+    content_length: 46714
     host: ""

internal/agent/testdata/TestCoderAgent/anthropic-sonnet/multiedit_tool.yaml 🔗

@@ -25,52 +25,52 @@ interactions:
     content_length: -1
     body: |+
       event: message_start
-      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_01FjZyvRmmP6f5huYC6M5w9q","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":170,"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"}} }
+      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_015YzXs6XaPmruFbmBEQmbzN","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":170,"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":"Edit"}              }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Edit"}           }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Go"} }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Go"}    }
 
       event: ping
       data: {"type": "ping"}
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Code"}}
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Code"}    }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Text"}          }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Greeting"}        }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Modification"}    }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Modification"}             }
 
       event: content_block_stop
       data: {"type":"content_block_stop","index":0      }
 
       event: message_delta
-      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":170,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":9}   }
+      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":170,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10}               }
 
       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: 614.803834ms
+    duration: 728.836709ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 46028
+    content_length: 46800
     host: ""

internal/agent/testdata/TestCoderAgent/anthropic-sonnet/parallel_tool_calls.yaml 🔗

@@ -25,55 +25,58 @@ interactions:
     content_length: -1
     body: |+
       event: message_start
-      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_01C8kCFF38gUc1F98cNSb1Je","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":159,"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":{"model":"claude-3-5-haiku-20241022","id":"msg_01PPi2LYDWFX14idF4WAuxdH","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":159,"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"}}        }
 
       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":"Parallel"}       }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Parallel"}}
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Go File"}            }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" File"}            }
 
       event: ping
       data: {"type": "ping"}
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Listing"}             }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Discovery"}  }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" an"} }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" with"}     }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d Directory"}             }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Glob"}            }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Scan"}              }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" and "}    }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"LS"}   }
 
       event: content_block_stop
-      data: {"type":"content_block_stop","index":0     }
+      data: {"type":"content_block_stop","index":0            }
 
       event: message_delta
-      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":159,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":13}              }
+      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":159,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":13}       }
 
       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: 640.574ms
+    duration: 774.758583ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 46039
+    content_length: 46811
     host: ""

internal/agent/testdata/TestCoderAgent/anthropic-sonnet/read_a_file.yaml 🔗

@@ -25,49 +25,49 @@ interactions:
     content_length: -1
     body: |+
       event: message_start
-      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_015jTnfBT3pBhzGcY78T5dtG","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":134,"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"}}   }
+      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_013ygFg57WnsbgYEgWKpUHgg","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":134,"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":"Rea"}           }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Rea"}               }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d Go"}            }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d Go"}             }
 
       event: ping
       data: {"type": "ping"}
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Module"}           }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Module"}         }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Details"}              }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Details"}        }
 
       event: content_block_stop
-      data: {"type":"content_block_stop","index":0     }
+      data: {"type":"content_block_stop","index":0         }
 
       event: message_delta
-      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":134,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":7}     }
+      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":134,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":7}       }
 
       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: 534.240625ms
+    duration: 543.64625ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 45912
+    content_length: 46684
     host: ""

internal/agent/testdata/TestCoderAgent/anthropic-sonnet/simple_test.yaml 🔗

@@ -25,46 +25,49 @@ interactions:
     content_length: -1
     body: |+
       event: message_start
-      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_01RjUuxns7MZVJP9xo7zXJpb","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":131,"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":{"model":"claude-3-5-haiku-20241022","id":"msg_01EmVyk4hid2PC6VniyWihaR","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":131,"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":"Quick"}  }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Receive"}           }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Greeting"}            }
 
       event: ping
       data: {"type": "ping"}
 
-      event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d"}            }
-
       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":131,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":7}}
+      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":131,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":6}          }
 
       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: 746.008542ms
+    duration: 1.063515458s
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 45902
+    content_length: 46674
     host: ""

internal/agent/testdata/TestCoderAgent/anthropic-sonnet/sourcegraph_tool.yaml 🔗

@@ -25,49 +25,52 @@ interactions:
     content_length: -1
     body: |+
       event: message_start
-      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_0195z3mYv7Lex96ZVV3g7ikE","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":145,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":3,"service_tier":"standard"}}          }
+      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_01KYmwLRQ7WjQjA2pbUzEhvU","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":145,"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"}} }
 
       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":"Searching Go"}            }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Go"}       }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Repos"}        }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Repo"}          }
 
       event: ping
       data: {"type": "ping"}
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" for Main"}       }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Main"}}
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Function"}      }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Functions"}        }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Search"}            }
 
       event: content_block_stop
-      data: {"type":"content_block_stop","index":0           }
+      data: {"type":"content_block_stop","index":0}
 
       event: message_delta
-      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":145,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":11}}
+      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":145,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":9}}
 
       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: 572.207834ms
+    duration: 1.002887292s
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 45962
+    content_length: 46734
     host: ""

internal/agent/testdata/TestCoderAgent/anthropic-sonnet/update_a_file.yaml 🔗

@@ -25,49 +25,49 @@ interactions:
     content_length: -1
     body: |+
       event: message_start
-      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_01A6Qs3G2Urq3zaAYhFxQNDW","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":145,"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"}}       }
+      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_01RwCAShrU1kRMXy4cvo5e5t","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":145,"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":"Update"}   }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Update"}        }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" main"}              }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" main"}       }
 
       event: ping
       data: {"type": "ping"}
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":".go Hello"} }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":".go Hello"}             }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Message"}      }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Message"}             }
 
       event: content_block_stop
-      data: {"type":"content_block_stop","index":0}
+      data: {"type":"content_block_stop","index":0       }
 
       event: message_delta
-      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":145,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":9}      }
+      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":145,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":9} }
 
       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: 552.206292ms
+    duration: 682.594041ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 45968
+    content_length: 46740
     host: ""

internal/agent/testdata/TestCoderAgent/anthropic-sonnet/write_tool.yaml 🔗

@@ -25,55 +25,52 @@ interactions:
     content_length: -1
     body: |+
       event: message_start
-      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_01BvsQtHoFHkXJ1tT8f3apHB","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":161,"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"}}    }
+      data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_01FXMC8aT9YaPtMV15mGZNsp","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":161,"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":"Creating"} }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Creating"}         }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" config"}            }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" config"}        }
 
       event: ping
       data: {"type": "ping"}
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":".json file"}            }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":".json with"}               }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" with"}   }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" JSON"}           }
 
       event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" JSON"}          }
-
-      event: content_block_delta
-      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" data"}  }
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" data"}              }
 
       event: content_block_stop
-      data: {"type":"content_block_stop","index":0      }
+      data: {"type":"content_block_stop","index":0               }
 
       event: message_delta
-      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":161,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":11}           }
+      data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":161,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10}  }
 
       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: 569.883ms
+    duration: 950.954959ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 46005
+    content_length: 46777
     host: ""

internal/agent/testdata/TestCoderAgent/openai-gpt-5/bash_tool.yaml 🔗

@@ -24,25 +24,23 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"chatcmpl-CVz85CQ0qlWRtpRhywMn7QxqYeUYQ","object":"chat.completion.chunk","created":1761739185,"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":"dPfRtCklVGPSLq"}
+      data: {"id":"chatcmpl-CWhWs20Zop6sbl4OStZokp9a0vd9d","object":"chat.completion.chunk","created":1761909858,"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":"pttO1OA8dsQGEG"}
 
-      data: {"id":"chatcmpl-CVz85CQ0qlWRtpRhywMn7QxqYeUYQ","object":"chat.completion.chunk","created":1761739185,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Create"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"3CGkCwblCY"}
+      data: {"id":"chatcmpl-CWhWs20Zop6sbl4OStZokp9a0vd9d","object":"chat.completion.chunk","created":1761909858,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Create"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"0RUeMk1U5O"}
 
-      data: {"id":"chatcmpl-CVz85CQ0qlWRtpRhywMn7QxqYeUYQ","object":"chat.completion.chunk","created":1761739185,"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":"oIjvexf0CnIPO7"}
+      data: {"id":"chatcmpl-CWhWs20Zop6sbl4OStZokp9a0vd9d","object":"chat.completion.chunk","created":1761909858,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" File"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wLi9GRw5fqJ"}
 
-      data: {"id":"chatcmpl-CVz85CQ0qlWRtpRhywMn7QxqYeUYQ","object":"chat.completion.chunk","created":1761739185,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" File"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"KshWvicCVvK"}
+      data: {"id":"chatcmpl-CWhWs20Zop6sbl4OStZokp9a0vd9d","object":"chat.completion.chunk","created":1761909858,"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":"21x6SCO5rY2"}
 
-      data: {"id":"chatcmpl-CVz85CQ0qlWRtpRhywMn7QxqYeUYQ","object":"chat.completion.chunk","created":1761739185,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"k72piYp6kyfVe"}
+      data: {"id":"chatcmpl-CWhWs20Zop6sbl4OStZokp9a0vd9d","object":"chat.completion.chunk","created":1761909858,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Content"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"0nwcSIkE"}
 
-      data: {"id":"chatcmpl-CVz85CQ0qlWRtpRhywMn7QxqYeUYQ","object":"chat.completion.chunk","created":1761739185,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Bash"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"C2cr4u4USNi"}
+      data: {"id":"chatcmpl-CWhWs20Zop6sbl4OStZokp9a0vd9d","object":"chat.completion.chunk","created":1761909858,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"txtgsExxlu5Ep"}
 
-      data: {"id":"chatcmpl-CVz85CQ0qlWRtpRhywMn7QxqYeUYQ","object":"chat.completion.chunk","created":1761739185,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Without"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"dI3i42bz"}
+      data: {"id":"chatcmpl-CWhWs20Zop6sbl4OStZokp9a0vd9d","object":"chat.completion.chunk","created":1761909858,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Bash"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"SouwjjVL0HG"}
 
-      data: {"id":"chatcmpl-CVz85CQ0qlWRtpRhywMn7QxqYeUYQ","object":"chat.completion.chunk","created":1761739185,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Timestamp"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"VdcMJZ"}
+      data: {"id":"chatcmpl-CWhWs20Zop6sbl4OStZokp9a0vd9d","object":"chat.completion.chunk","created":1761909858,"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":"UIjY7l3Fq2"}
 
-      data: {"id":"chatcmpl-CVz85CQ0qlWRtpRhywMn7QxqYeUYQ","object":"chat.completion.chunk","created":1761739185,"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":"i4Wcz3RpqM"}
-
-      data: {"id":"chatcmpl-CVz85CQ0qlWRtpRhywMn7QxqYeUYQ","object":"chat.completion.chunk","created":1761739185,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":145,"completion_tokens":7,"total_tokens":152,"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":"wLpzGFATixr61S"}
+      data: {"id":"chatcmpl-CWhWs20Zop6sbl4OStZokp9a0vd9d","object":"chat.completion.chunk","created":1761909858,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":145,"completion_tokens":6,"total_tokens":151,"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":"FzDWgx8tnMDVXf"}
 
       data: [DONE]
 
@@ -51,15 +49,15 @@ interactions:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 483.992958ms
+    duration: 526.4125ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44434
+    content_length: 45186
     host: ""

internal/agent/testdata/TestCoderAgent/openai-gpt-5/download_tool.yaml 🔗

@@ -24,25 +24,23 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"chatcmpl-CVz8ORCoQnKn39RNjD1Lf8o75Wf0z","object":"chat.completion.chunk","created":1761739204,"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":"RwvYFz0dPL8l0W"}
+      data: {"id":"chatcmpl-CWhX9tjyqID6dcImwmtScsTH2gMCA","object":"chat.completion.chunk","created":1761909875,"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":"pRl8hi4mKyV7v4"}
 
-      data: {"id":"chatcmpl-CVz8ORCoQnKn39RNjD1Lf8o75Wf0z","object":"chat.completion.chunk","created":1761739204,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Downloading"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"CqJV6"}
+      data: {"id":"chatcmpl-CWhX9tjyqID6dcImwmtScsTH2gMCA","object":"chat.completion.chunk","created":1761909875,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Download"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"6Ty98YpI"}
 
-      data: {"id":"chatcmpl-CVz8ORCoQnKn39RNjD1Lf8o75Wf0z","object":"chat.completion.chunk","created":1761739204,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"MPLHEWMpGNMy"}
+      data: {"id":"chatcmpl-CWhX9tjyqID6dcImwmtScsTH2gMCA","object":"chat.completion.chunk","created":1761909875,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"9QyOAidqj1g1"}
 
-      data: {"id":"chatcmpl-CVz8ORCoQnKn39RNjD1Lf8o75Wf0z","object":"chat.completion.chunk","created":1761739204,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Saving"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"OfMw5WYWH"}
+      data: {"id":"chatcmpl-CWhX9tjyqID6dcImwmtScsTH2gMCA","object":"chat.completion.chunk","created":1761909875,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Save"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"vN0Iq39KAX0"}
 
-      data: {"id":"chatcmpl-CVz8ORCoQnKn39RNjD1Lf8o75Wf0z","object":"chat.completion.chunk","created":1761739204,"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":"o3iIHjAqucSGGm"}
+      data: {"id":"chatcmpl-CWhX9tjyqID6dcImwmtScsTH2gMCA","object":"chat.completion.chunk","created":1761909875,"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":"SUk4BJZ9jFVGsF"}
 
-      data: {"id":"chatcmpl-CVz8ORCoQnKn39RNjD1Lf8o75Wf0z","object":"chat.completion.chunk","created":1761739204,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" File"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"rxRYblxgwlo"}
+      data: {"id":"chatcmpl-CWhX9tjyqID6dcImwmtScsTH2gMCA","object":"chat.completion.chunk","created":1761909875,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Text"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"9Ce03nGGaDK"}
 
-      data: {"id":"chatcmpl-CVz8ORCoQnKn39RNjD1Lf8o75Wf0z","object":"chat.completion.chunk","created":1761739204,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" from"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"KSaPxJ74eiy"}
+      data: {"id":"chatcmpl-CWhX9tjyqID6dcImwmtScsTH2gMCA","object":"chat.completion.chunk","created":1761909875,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" File"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"GYeSvnujasB"}
 
-      data: {"id":"chatcmpl-CVz8ORCoQnKn39RNjD1Lf8o75Wf0z","object":"chat.completion.chunk","created":1761739204,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" URL"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"NTcNYugpWc6J"}
+      data: {"id":"chatcmpl-CWhX9tjyqID6dcImwmtScsTH2gMCA","object":"chat.completion.chunk","created":1761909875,"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":"tW1ZfW5I0o"}
 
-      data: {"id":"chatcmpl-CVz8ORCoQnKn39RNjD1Lf8o75Wf0z","object":"chat.completion.chunk","created":1761739204,"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":"ZdzoBm7g8D"}
-
-      data: {"id":"chatcmpl-CVz8ORCoQnKn39RNjD1Lf8o75Wf0z","object":"chat.completion.chunk","created":1761739204,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":148,"completion_tokens":7,"total_tokens":155,"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":"TbRv7pp8ket1UJ"}
+      data: {"id":"chatcmpl-CWhX9tjyqID6dcImwmtScsTH2gMCA","object":"chat.completion.chunk","created":1761909875,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":148,"completion_tokens":6,"total_tokens":154,"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":"KCYAxQFjHWFd2L"}
 
       data: [DONE]
 
@@ -51,15 +49,15 @@ interactions:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 563.767708ms
+    duration: 515.942833ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44459
+    content_length: 45211
     host: ""

internal/agent/testdata/TestCoderAgent/openai-gpt-5/fetch_tool.yaml 🔗

@@ -24,31 +24,25 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"chatcmpl-CVz8Z1N4lDfIKxM3aKQXbLTiNKzfZ","object":"chat.completion.chunk","created":1761739215,"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":"ZSodjBgdEw6KCJ"}
+      data: {"id":"chatcmpl-CWhXOu4oZ9WdLk0MqlX7ErUyg1LEH","object":"chat.completion.chunk","created":1761909890,"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":"NmqSJ50vbN4r42"}
 
-      data: {"id":"chatcmpl-CVz8Z1N4lDfIKxM3aKQXbLTiNKzfZ","object":"chat.completion.chunk","created":1761739215,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Checking"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"bF12rabT"}
+      data: {"id":"chatcmpl-CWhXOu4oZ9WdLk0MqlX7ErUyg1LEH","object":"chat.completion.chunk","created":1761909890,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Check"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"sr4OJu3lLyu"}
 
-      data: {"id":"chatcmpl-CVz8Z1N4lDfIKxM3aKQXbLTiNKzfZ","object":"chat.completion.chunk","created":1761739215,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" if"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Qfmp4QtXXMBJ2"}
+      data: {"id":"chatcmpl-CWhXOu4oZ9WdLk0MqlX7ErUyg1LEH","object":"chat.completion.chunk","created":1761909890,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" HTML"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"KmaFu3GGouR"}
 
-      data: {"id":"chatcmpl-CVz8Z1N4lDfIKxM3aKQXbLTiNKzfZ","object":"chat.completion.chunk","created":1761739215,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" '"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"HFTQeLI6UiLT25"}
+      data: {"id":"chatcmpl-CWhXOu4oZ9WdLk0MqlX7ErUyg1LEH","object":"chat.completion.chunk","created":1761909890,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"H245qWL5SGiq"}
 
-      data: {"id":"chatcmpl-CVz8Z1N4lDfIKxM3aKQXbLTiNKzfZ","object":"chat.completion.chunk","created":1761739215,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"John"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"UI4PYSxRbpnM"}
+      data: {"id":"chatcmpl-CWhXOu4oZ9WdLk0MqlX7ErUyg1LEH","object":"chat.completion.chunk","created":1761909890,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" John"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ZUvzYSFf8lB"}
 
-      data: {"id":"chatcmpl-CVz8Z1N4lDfIKxM3aKQXbLTiNKzfZ","object":"chat.completion.chunk","created":1761739215,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Doe"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ojMKfmvfUFg4"}
+      data: {"id":"chatcmpl-CWhXOu4oZ9WdLk0MqlX7ErUyg1LEH","object":"chat.completion.chunk","created":1761909890,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Doe"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"s8QJh8k4MHNa"}
 
-      data: {"id":"chatcmpl-CVz8Z1N4lDfIKxM3aKQXbLTiNKzfZ","object":"chat.completion.chunk","created":1761739215,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"'"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"qkbwGKQtasDxjsc"}
+      data: {"id":"chatcmpl-CWhXOu4oZ9WdLk0MqlX7ErUyg1LEH","object":"chat.completion.chunk","created":1761909890,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Occ"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"5isKdqUb0HOD"}
 
-      data: {"id":"chatcmpl-CVz8Z1N4lDfIKxM3aKQXbLTiNKzfZ","object":"chat.completion.chunk","created":1761739215,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Exists"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"PWMxS15Ep"}
+      data: {"id":"chatcmpl-CWhXOu4oZ9WdLk0MqlX7ErUyg1LEH","object":"chat.completion.chunk","created":1761909890,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"urrence"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ghVyn4TaC"}
 
-      data: {"id":"chatcmpl-CVz8Z1N4lDfIKxM3aKQXbLTiNKzfZ","object":"chat.completion.chunk","created":1761739215,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" on"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"8AEgYShpeKANP"}
+      data: {"id":"chatcmpl-CWhXOu4oZ9WdLk0MqlX7ErUyg1LEH","object":"chat.completion.chunk","created":1761909890,"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":"IQcLjlqYzI"}
 
-      data: {"id":"chatcmpl-CVz8Z1N4lDfIKxM3aKQXbLTiNKzfZ","object":"chat.completion.chunk","created":1761739215,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Example"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"i5ULrBVE"}
-
-      data: {"id":"chatcmpl-CVz8Z1N4lDfIKxM3aKQXbLTiNKzfZ","object":"chat.completion.chunk","created":1761739215,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" HTML"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"b1Y4cutYF16"}
-
-      data: {"id":"chatcmpl-CVz8Z1N4lDfIKxM3aKQXbLTiNKzfZ","object":"chat.completion.chunk","created":1761739215,"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":"7b6R8Pa8K7"}
-
-      data: {"id":"chatcmpl-CVz8Z1N4lDfIKxM3aKQXbLTiNKzfZ","object":"chat.completion.chunk","created":1761739215,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":153,"completion_tokens":10,"total_tokens":163,"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":"NJ9Mpy4WpGAPz"}
+      data: {"id":"chatcmpl-CWhXOu4oZ9WdLk0MqlX7ErUyg1LEH","object":"chat.completion.chunk","created":1761909890,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":153,"completion_tokens":7,"total_tokens":160,"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":"4FTCmELUOWr1y1"}
 
       data: [DONE]
 
@@ -57,15 +51,15 @@ interactions:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 466.436917ms
+    duration: 532.60175ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44477
+    content_length: 45229
     host: ""

internal/agent/testdata/TestCoderAgent/openai-gpt-5/glob_tool.yaml 🔗

@@ -24,29 +24,29 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"chatcmpl-CVz8nkACA1RsQpVgdlsFtHBkPgEjH","object":"chat.completion.chunk","created":1761739229,"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":"E7kaF9NObIZFla"}
+      data: {"id":"chatcmpl-CWhXWSMCr6qB3gnWTLgyqU7xhyNfD","object":"chat.completion.chunk","created":1761909898,"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":"nUGlLYeV4PG3P7"}
 
-      data: {"id":"chatcmpl-CVz8nkACA1RsQpVgdlsFtHBkPgEjH","object":"chat.completion.chunk","created":1761739229,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Finding"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"VbMVqy1KJ"}
+      data: {"id":"chatcmpl-CWhXWSMCr6qB3gnWTLgyqU7xhyNfD","object":"chat.completion.chunk","created":1761909898,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Finding"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"aPwkd1hKI"}
 
-      data: {"id":"chatcmpl-CVz8nkACA1RsQpVgdlsFtHBkPgEjH","object":"chat.completion.chunk","created":1761739229,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" ."},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"eWVVzxYqslpGup"}
+      data: {"id":"chatcmpl-CWhXWSMCr6qB3gnWTLgyqU7xhyNfD","object":"chat.completion.chunk","created":1761909898,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" ."},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"lzKg5YVSFHVwma"}
 
-      data: {"id":"chatcmpl-CVz8nkACA1RsQpVgdlsFtHBkPgEjH","object":"chat.completion.chunk","created":1761739229,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"go"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"yuwvS6J1n7FYdl"}
+      data: {"id":"chatcmpl-CWhXWSMCr6qB3gnWTLgyqU7xhyNfD","object":"chat.completion.chunk","created":1761909898,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"go"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"BEOfuOOPpuDTu1"}
 
-      data: {"id":"chatcmpl-CVz8nkACA1RsQpVgdlsFtHBkPgEjH","object":"chat.completion.chunk","created":1761739229,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Files"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"nWBgo5PP8U"}
+      data: {"id":"chatcmpl-CWhXWSMCr6qB3gnWTLgyqU7xhyNfD","object":"chat.completion.chunk","created":1761909898,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Files"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"H5gIqlzUGQ"}
 
-      data: {"id":"chatcmpl-CVz8nkACA1RsQpVgdlsFtHBkPgEjH","object":"chat.completion.chunk","created":1761739229,"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":"jfCy4U9cFjj"}
+      data: {"id":"chatcmpl-CWhXWSMCr6qB3gnWTLgyqU7xhyNfD","object":"chat.completion.chunk","created":1761909898,"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":"Q63P87LfKEJ"}
 
-      data: {"id":"chatcmpl-CVz8nkACA1RsQpVgdlsFtHBkPgEjH","object":"chat.completion.chunk","created":1761739229,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Glob"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"KKQl6QiQWnt"}
+      data: {"id":"chatcmpl-CWhXWSMCr6qB3gnWTLgyqU7xhyNfD","object":"chat.completion.chunk","created":1761909898,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Glob"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"GoNoWnJtfSk"}
 
-      data: {"id":"chatcmpl-CVz8nkACA1RsQpVgdlsFtHBkPgEjH","object":"chat.completion.chunk","created":1761739229,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"HxbWz6VBDUpff"}
+      data: {"id":"chatcmpl-CWhXWSMCr6qB3gnWTLgyqU7xhyNfD","object":"chat.completion.chunk","created":1761909898,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"tX6giDfA1VUuB"}
 
-      data: {"id":"chatcmpl-CVz8nkACA1RsQpVgdlsFtHBkPgEjH","object":"chat.completion.chunk","created":1761739229,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Current"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"QLwGYffq"}
+      data: {"id":"chatcmpl-CWhXWSMCr6qB3gnWTLgyqU7xhyNfD","object":"chat.completion.chunk","created":1761909898,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Current"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"lpFxnTag"}
 
-      data: {"id":"chatcmpl-CVz8nkACA1RsQpVgdlsFtHBkPgEjH","object":"chat.completion.chunk","created":1761739229,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Directory"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"SXUCMF"}
+      data: {"id":"chatcmpl-CWhXWSMCr6qB3gnWTLgyqU7xhyNfD","object":"chat.completion.chunk","created":1761909898,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Directory"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"sFvWcn"}
 
-      data: {"id":"chatcmpl-CVz8nkACA1RsQpVgdlsFtHBkPgEjH","object":"chat.completion.chunk","created":1761739229,"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":"XHDEivoJLE"}
+      data: {"id":"chatcmpl-CWhXWSMCr6qB3gnWTLgyqU7xhyNfD","object":"chat.completion.chunk","created":1761909898,"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":"3bg73MZKl9"}
 
-      data: {"id":"chatcmpl-CVz8nkACA1RsQpVgdlsFtHBkPgEjH","object":"chat.completion.chunk","created":1761739229,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":137,"completion_tokens":9,"total_tokens":146,"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":"ZQpOdtfy9BHSzt"}
+      data: {"id":"chatcmpl-CWhXWSMCr6qB3gnWTLgyqU7xhyNfD","object":"chat.completion.chunk","created":1761909898,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":137,"completion_tokens":9,"total_tokens":146,"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":"pSfi02ffdiVgGo"}
 
       data: [DONE]
 
@@ -55,15 +55,15 @@ interactions:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 672.036958ms
+    duration: 420.291ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44395
+    content_length: 45147
     host: ""

internal/agent/testdata/TestCoderAgent/openai-gpt-5/grep_tool.yaml 🔗

@@ -24,33 +24,31 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"chatcmpl-CVz8u8rtmrPrnOnMfyPeNyeEqUUrD","object":"chat.completion.chunk","created":1761739236,"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":"ahKhyTWfKpnSNX"}
+      data: {"id":"chatcmpl-CWhXc3eVJlBmjRe4RlplK8Xv5mR1Z","object":"chat.completion.chunk","created":1761909904,"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":"GoaiUY1uzWtKYt"}
 
-      data: {"id":"chatcmpl-CVz8u8rtmrPrnOnMfyPeNyeEqUUrD","object":"chat.completion.chunk","created":1761739236,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Using"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"OMUcUyQXmXS"}
+      data: {"id":"chatcmpl-CWhXc3eVJlBmjRe4RlplK8Xv5mR1Z","object":"chat.completion.chunk","created":1761909904,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Search"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ZAPV7XTqck"}
 
-      data: {"id":"chatcmpl-CVz8u8rtmrPrnOnMfyPeNyeEqUUrD","object":"chat.completion.chunk","created":1761739236,"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":"ExmDSgnleUNM"}
+      data: {"id":"chatcmpl-CWhXc3eVJlBmjRe4RlplK8Xv5mR1Z","object":"chat.completion.chunk","created":1761909904,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"J8yw8jrWnNUr"}
 
-      data: {"id":"chatcmpl-CVz8u8rtmrPrnOnMfyPeNyeEqUUrD","object":"chat.completion.chunk","created":1761739236,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"p"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"su01FSZqFiUkvbC"}
+      data: {"id":"chatcmpl-CWhXc3eVJlBmjRe4RlplK8Xv5mR1Z","object":"chat.completion.chunk","created":1761909904,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" '"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"XdxqK7R5JFw0F5"}
 
-      data: {"id":"chatcmpl-CVz8u8rtmrPrnOnMfyPeNyeEqUUrD","object":"chat.completion.chunk","created":1761739236,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"tcKXxtEspOODx"}
+      data: {"id":"chatcmpl-CWhXc3eVJlBmjRe4RlplK8Xv5mR1Z","object":"chat.completion.chunk","created":1761909904,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"package"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wkBDR8dQf"}
 
-      data: {"id":"chatcmpl-CVz8u8rtmrPrnOnMfyPeNyeEqUUrD","object":"chat.completion.chunk","created":1761739236,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Find"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"SnppB5u8iZh"}
+      data: {"id":"chatcmpl-CWhXc3eVJlBmjRe4RlplK8Xv5mR1Z","object":"chat.completion.chunk","created":1761909904,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"'"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"vXtbve7G3a8maN4"}
 
-      data: {"id":"chatcmpl-CVz8u8rtmrPrnOnMfyPeNyeEqUUrD","object":"chat.completion.chunk","created":1761739236,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" '"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"6YYmrtrzGibewT"}
+      data: {"id":"chatcmpl-CWhXc3eVJlBmjRe4RlplK8Xv5mR1Z","object":"chat.completion.chunk","created":1761909904,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"8wf45VJIxwFsZ"}
 
-      data: {"id":"chatcmpl-CVz8u8rtmrPrnOnMfyPeNyeEqUUrD","object":"chat.completion.chunk","created":1761739236,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Package"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"okrR2yd76"}
+      data: {"id":"chatcmpl-CWhXc3eVJlBmjRe4RlplK8Xv5mR1Z","object":"chat.completion.chunk","created":1761909904,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Go"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"pRsCVMZRQ070q"}
 
-      data: {"id":"chatcmpl-CVz8u8rtmrPrnOnMfyPeNyeEqUUrD","object":"chat.completion.chunk","created":1761739236,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"'"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"GiDYz9Ujv3CEhdo"}
+      data: {"id":"chatcmpl-CWhXc3eVJlBmjRe4RlplK8Xv5mR1Z","object":"chat.completion.chunk","created":1761909904,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" files"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"x7uOQzDFpI"}
 
-      data: {"id":"chatcmpl-CVz8u8rtmrPrnOnMfyPeNyeEqUUrD","object":"chat.completion.chunk","created":1761739236,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"H0dMz9bYeCrQV"}
+      data: {"id":"chatcmpl-CWhXc3eVJlBmjRe4RlplK8Xv5mR1Z","object":"chat.completion.chunk","created":1761909904,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" using"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"2IWrOTMyMS"}
 
-      data: {"id":"chatcmpl-CVz8u8rtmrPrnOnMfyPeNyeEqUUrD","object":"chat.completion.chunk","created":1761739236,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Go"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"qXNyYxUt2IC7r"}
+      data: {"id":"chatcmpl-CWhXc3eVJlBmjRe4RlplK8Xv5mR1Z","object":"chat.completion.chunk","created":1761909904,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" grep"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"KxgMgUqrUvo"}
 
-      data: {"id":"chatcmpl-CVz8u8rtmrPrnOnMfyPeNyeEqUUrD","object":"chat.completion.chunk","created":1761739236,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Files"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"k4yGSJkTED"}
+      data: {"id":"chatcmpl-CWhXc3eVJlBmjRe4RlplK8Xv5mR1Z","object":"chat.completion.chunk","created":1761909904,"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":"7ovkOmaQwj"}
 
-      data: {"id":"chatcmpl-CVz8u8rtmrPrnOnMfyPeNyeEqUUrD","object":"chat.completion.chunk","created":1761739236,"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":"dEm4izpMZ3"}
-
-      data: {"id":"chatcmpl-CVz8u8rtmrPrnOnMfyPeNyeEqUUrD","object":"chat.completion.chunk","created":1761739236,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":138,"completion_tokens":11,"total_tokens":149,"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":"6OFFvxMCHtM5j"}
+      data: {"id":"chatcmpl-CWhXc3eVJlBmjRe4RlplK8Xv5mR1Z","object":"chat.completion.chunk","created":1761909904,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":138,"completion_tokens":10,"total_tokens":148,"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":"7o8VW7i5JEpVy"}
 
       data: [DONE]
 
@@ -59,15 +57,15 @@ interactions:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 908.098416ms
+    duration: 1.038688833s
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44393
+    content_length: 45145
     host: ""

internal/agent/testdata/TestCoderAgent/openai-gpt-5/ls_tool.yaml 🔗

@@ -24,25 +24,31 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"chatcmpl-CVz93jAyx5CeFxsv9A8SSZpefq5qN","object":"chat.completion.chunk","created":1761739245,"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":"5QixgozlJLHplQ"}
+      data: {"id":"chatcmpl-CWhXkFTjwl5xUXnehGUg9PaN6qNq8","object":"chat.completion.chunk","created":1761909912,"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":"MCWFcLhdK58FbJ"}
 
-      data: {"id":"chatcmpl-CVz93jAyx5CeFxsv9A8SSZpefq5qN","object":"chat.completion.chunk","created":1761739245,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"List"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"u8ABYV1Emeo0"}
+      data: {"id":"chatcmpl-CWhXkFTjwl5xUXnehGUg9PaN6qNq8","object":"chat.completion.chunk","created":1761909912,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Using"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"3EBKxRkyeGg"}
 
-      data: {"id":"chatcmpl-CVz93jAyx5CeFxsv9A8SSZpefq5qN","object":"chat.completion.chunk","created":1761739245,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Files"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"S951WhiNf6"}
+      data: {"id":"chatcmpl-CWhXkFTjwl5xUXnehGUg9PaN6qNq8","object":"chat.completion.chunk","created":1761909912,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" '"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xIi5UalBhOwMbg"}
 
-      data: {"id":"chatcmpl-CVz93jAyx5CeFxsv9A8SSZpefq5qN","object":"chat.completion.chunk","created":1761739245,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"YudcskZEywucf"}
+      data: {"id":"chatcmpl-CWhXkFTjwl5xUXnehGUg9PaN6qNq8","object":"chat.completion.chunk","created":1761909912,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"ls"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"CAVw4k1WmiwjLa"}
 
-      data: {"id":"chatcmpl-CVz93jAyx5CeFxsv9A8SSZpefq5qN","object":"chat.completion.chunk","created":1761739245,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Directory"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"tjJf66"}
+      data: {"id":"chatcmpl-CWhXkFTjwl5xUXnehGUg9PaN6qNq8","object":"chat.completion.chunk","created":1761909912,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"'"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"F3Bq812chIUIzGX"}
 
-      data: {"id":"chatcmpl-CVz93jAyx5CeFxsv9A8SSZpefq5qN","object":"chat.completion.chunk","created":1761739245,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Using"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"6NpmB69Xbd"}
+      data: {"id":"chatcmpl-CWhXkFTjwl5xUXnehGUg9PaN6qNq8","object":"chat.completion.chunk","created":1761909912,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" to"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"95ULPKgMvp7To"}
 
-      data: {"id":"chatcmpl-CVz93jAyx5CeFxsv9A8SSZpefq5qN","object":"chat.completion.chunk","created":1761739245,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" ls"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"S1TPsqQSetufh"}
+      data: {"id":"chatcmpl-CWhXkFTjwl5xUXnehGUg9PaN6qNq8","object":"chat.completion.chunk","created":1761909912,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" List"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"r5GrJW1wmqP"}
 
-      data: {"id":"chatcmpl-CVz93jAyx5CeFxsv9A8SSZpefq5qN","object":"chat.completion.chunk","created":1761739245,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Command"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"kt6iCrf5"}
+      data: {"id":"chatcmpl-CWhXkFTjwl5xUXnehGUg9PaN6qNq8","object":"chat.completion.chunk","created":1761909912,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Files"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"MxdLTHikvT"}
 
-      data: {"id":"chatcmpl-CVz93jAyx5CeFxsv9A8SSZpefq5qN","object":"chat.completion.chunk","created":1761739245,"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":"hcQCwYw5aa"}
+      data: {"id":"chatcmpl-CWhXkFTjwl5xUXnehGUg9PaN6qNq8","object":"chat.completion.chunk","created":1761909912,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"QGNfitR1P70YY"}
 
-      data: {"id":"chatcmpl-CVz93jAyx5CeFxsv9A8SSZpefq5qN","object":"chat.completion.chunk","created":1761739245,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":135,"completion_tokens":7,"total_tokens":142,"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":"NTja3di3JtfoUc"}
+      data: {"id":"chatcmpl-CWhXkFTjwl5xUXnehGUg9PaN6qNq8","object":"chat.completion.chunk","created":1761909912,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Current"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"sUeNiG4y"}
+
+      data: {"id":"chatcmpl-CWhXkFTjwl5xUXnehGUg9PaN6qNq8","object":"chat.completion.chunk","created":1761909912,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Directory"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"J0RDzd"}
+
+      data: {"id":"chatcmpl-CWhXkFTjwl5xUXnehGUg9PaN6qNq8","object":"chat.completion.chunk","created":1761909912,"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":"5QOUpMs2Ml"}
+
+      data: {"id":"chatcmpl-CWhXkFTjwl5xUXnehGUg9PaN6qNq8","object":"chat.completion.chunk","created":1761909912,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":135,"completion_tokens":10,"total_tokens":145,"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":"vFQ6uI3Cg2SNR"}
 
       data: [DONE]
 
@@ -51,15 +57,15 @@ interactions:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 516.491041ms
+    duration: 426.932792ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44387
+    content_length: 45139
     host: ""

internal/agent/testdata/TestCoderAgent/openai-gpt-5/multiedit_tool.yaml 🔗

@@ -24,27 +24,35 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"chatcmpl-CVz9BYtDNy4C04jozEiPEmpYjut38","object":"chat.completion.chunk","created":1761739253,"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":"A5mPC2w49X7e4L"}
+      data: {"id":"chatcmpl-CWhXqpTXLkin7AKNshvJ7Z7SfYWRL","object":"chat.completion.chunk","created":1761909918,"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":"vib8DrjqB0M7hn"}
 
-      data: {"id":"chatcmpl-CVz9BYtDNy4C04jozEiPEmpYjut38","object":"chat.completion.chunk","created":1761739253,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Code"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"SS1H1FSBIjyj"}
+      data: {"id":"chatcmpl-CWhXqpTXLkin7AKNshvJ7Z7SfYWRL","object":"chat.completion.chunk","created":1761909918,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Modify"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"1k07w3fzQu"}
 
-      data: {"id":"chatcmpl-CVz9BYtDNy4C04jozEiPEmpYjut38","object":"chat.completion.chunk","created":1761739253,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Update"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Ib9alVmik"}
+      data: {"id":"chatcmpl-CWhXqpTXLkin7AKNshvJ7Z7SfYWRL","object":"chat.completion.chunk","created":1761909918,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" '"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"GyM9bWqkLw92Vr"}
 
-      data: {"id":"chatcmpl-CVz9BYtDNy4C04jozEiPEmpYjut38","object":"chat.completion.chunk","created":1761739253,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":":"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"eyZSLrijBAyTFBv"}
+      data: {"id":"chatcmpl-CWhXqpTXLkin7AKNshvJ7Z7SfYWRL","object":"chat.completion.chunk","created":1761909918,"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":"CUtYPUorfpS"}
 
-      data: {"id":"chatcmpl-CVz9BYtDNy4C04jozEiPEmpYjut38","object":"chat.completion.chunk","created":1761739253,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Modify"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"AB1TfytG6"}
+      data: {"id":"chatcmpl-CWhXqpTXLkin7AKNshvJ7Z7SfYWRL","object":"chat.completion.chunk","created":1761909918,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"qJjRwbZDfVEbChS"}
 
-      data: {"id":"chatcmpl-CVz9BYtDNy4C04jozEiPEmpYjut38","object":"chat.completion.chunk","created":1761739253,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Greeting"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Kw2tOAz"}
+      data: {"id":"chatcmpl-CWhXqpTXLkin7AKNshvJ7Z7SfYWRL","object":"chat.completion.chunk","created":1761909918,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" World"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"a8xCMyxcxi"}
 
-      data: {"id":"chatcmpl-CVz9BYtDNy4C04jozEiPEmpYjut38","object":"chat.completion.chunk","created":1761739253,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"J2VONZuhdNWT"}
+      data: {"id":"chatcmpl-CWhXqpTXLkin7AKNshvJ7Z7SfYWRL","object":"chat.completion.chunk","created":1761909918,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"!'"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"kaNX4o0E836bQf"}
 
-      data: {"id":"chatcmpl-CVz9BYtDNy4C04jozEiPEmpYjut38","object":"chat.completion.chunk","created":1761739253,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Add"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"rwzswcLQzBX3"}
+      data: {"id":"chatcmpl-CWhXqpTXLkin7AKNshvJ7Z7SfYWRL","object":"chat.completion.chunk","created":1761909918,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"KZw6b8gqUCMF"}
 
-      data: {"id":"chatcmpl-CVz9BYtDNy4C04jozEiPEmpYjut38","object":"chat.completion.chunk","created":1761739253,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Comment"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ynC8NzJt"}
+      data: {"id":"chatcmpl-CWhXqpTXLkin7AKNshvJ7Z7SfYWRL","object":"chat.completion.chunk","created":1761909918,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Add"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"C7aH71QYiC4X"}
 
-      data: {"id":"chatcmpl-CVz9BYtDNy4C04jozEiPEmpYjut38","object":"chat.completion.chunk","created":1761739253,"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":"IzNrxqGDIw"}
+      data: {"id":"chatcmpl-CWhXqpTXLkin7AKNshvJ7Z7SfYWRL","object":"chat.completion.chunk","created":1761909918,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Comment"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"rLwLkf9r"}
 
-      data: {"id":"chatcmpl-CVz9BYtDNy4C04jozEiPEmpYjut38","object":"chat.completion.chunk","created":1761739253,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":157,"completion_tokens":8,"total_tokens":165,"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":"tMyXWmzN6vgaQN"}
+      data: {"id":"chatcmpl-CWhXqpTXLkin7AKNshvJ7Z7SfYWRL","object":"chat.completion.chunk","created":1761909918,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"2jC9m9myYQL9E"}
+
+      data: {"id":"chatcmpl-CWhXqpTXLkin7AKNshvJ7Z7SfYWRL","object":"chat.completion.chunk","created":1761909918,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" main"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"G4ZfwxDqjIx"}
+
+      data: {"id":"chatcmpl-CWhXqpTXLkin7AKNshvJ7Z7SfYWRL","object":"chat.completion.chunk","created":1761909918,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":".go"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"zOlVgRrJ3tTdH"}
+
+      data: {"id":"chatcmpl-CWhXqpTXLkin7AKNshvJ7Z7SfYWRL","object":"chat.completion.chunk","created":1761909918,"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":"E7YvkSyrAi"}
+
+      data: {"id":"chatcmpl-CWhXqpTXLkin7AKNshvJ7Z7SfYWRL","object":"chat.completion.chunk","created":1761909918,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":157,"completion_tokens":12,"total_tokens":169,"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":"gDJn2sP6sddQe"}
 
       data: [DONE]
 
@@ -53,15 +61,15 @@ interactions:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 652.283291ms
+    duration: 911.194292ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44473
+    content_length: 45225
     host: ""

internal/agent/testdata/TestCoderAgent/openai-gpt-5/parallel_tool_calls.yaml 🔗

@@ -24,31 +24,29 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"chatcmpl-CVzA7h3O81W7Kcav9cNqmaQrk7Pio","object":"chat.completion.chunk","created":1761739311,"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":"fBW8ERonoDjPp2"}
+      data: {"id":"chatcmpl-CWhZJC0z3DUeuu1TL3VBI1UmbDPr2","object":"chat.completion.chunk","created":1761910009,"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":"7Cz8ZXHoTLl4gu"}
 
-      data: {"id":"chatcmpl-CVzA7h3O81W7Kcav9cNqmaQrk7Pio","object":"chat.completion.chunk","created":1761739311,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Run"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"NOY0xKDAXaeZT"}
+      data: {"id":"chatcmpl-CWhZJC0z3DUeuu1TL3VBI1UmbDPr2","object":"chat.completion.chunk","created":1761910009,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Run"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Kk1HPfHLJ3CkO"}
 
-      data: {"id":"chatcmpl-CVzA7h3O81W7Kcav9cNqmaQrk7Pio","object":"chat.completion.chunk","created":1761739311,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" glob"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"fUFLgRivvgn"}
+      data: {"id":"chatcmpl-CWhZJC0z3DUeuu1TL3VBI1UmbDPr2","object":"chat.completion.chunk","created":1761910009,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Glob"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"DR9fbWtQCxy"}
 
-      data: {"id":"chatcmpl-CVzA7h3O81W7Kcav9cNqmaQrk7Pio","object":"chat.completion.chunk","created":1761739311,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"34SQZ5YlfwUq"}
+      data: {"id":"chatcmpl-CWhZJC0z3DUeuu1TL3VBI1UmbDPr2","object":"chat.completion.chunk","created":1761910009,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" and"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"PftnV31ZNOAo"}
 
-      data: {"id":"chatcmpl-CVzA7h3O81W7Kcav9cNqmaQrk7Pio","object":"chat.completion.chunk","created":1761739311,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" ls"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"BtIV3GCECJAvH"}
+      data: {"id":"chatcmpl-CWhZJC0z3DUeuu1TL3VBI1UmbDPr2","object":"chat.completion.chunk","created":1761910009,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" LS"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"NMfENqXfVuow8"}
 
-      data: {"id":"chatcmpl-CVzA7h3O81W7Kcav9cNqmaQrk7Pio","object":"chat.completion.chunk","created":1761739311,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"odxkkaok1GA0n"}
+      data: {"id":"chatcmpl-CWhZJC0z3DUeuu1TL3VBI1UmbDPr2","object":"chat.completion.chunk","created":1761910009,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"i7QWCjqbnVkyH"}
 
-      data: {"id":"chatcmpl-CVzA7h3O81W7Kcav9cNqmaQrk7Pio","object":"chat.completion.chunk","created":1761739311,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" parallel"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"UTPm4Nz"}
+      data: {"id":"chatcmpl-CWhZJC0z3DUeuu1TL3VBI1UmbDPr2","object":"chat.completion.chunk","created":1761910009,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Parallel"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"V27SfNN"}
 
-      data: {"id":"chatcmpl-CVzA7h3O81W7Kcav9cNqmaQrk7Pio","object":"chat.completion.chunk","created":1761739311,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"1bSYd379FCmf"}
+      data: {"id":"chatcmpl-CWhZJC0z3DUeuu1TL3VBI1UmbDPr2","object":"chat.completion.chunk","created":1761910009,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"OFLkXlJfMYgH"}
 
-      data: {"id":"chatcmpl-CVzA7h3O81W7Kcav9cNqmaQrk7Pio","object":"chat.completion.chunk","created":1761739311,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" ."},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"dHc9swaGTqacpR"}
+      data: {"id":"chatcmpl-CWhZJC0z3DUeuu1TL3VBI1UmbDPr2","object":"chat.completion.chunk","created":1761910009,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Go"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"h0JrPavopghNT"}
 
-      data: {"id":"chatcmpl-CVzA7h3O81W7Kcav9cNqmaQrk7Pio","object":"chat.completion.chunk","created":1761739311,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"go"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"jaW5KAJBdyX7qt"}
+      data: {"id":"chatcmpl-CWhZJC0z3DUeuu1TL3VBI1UmbDPr2","object":"chat.completion.chunk","created":1761910009,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Files"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"UgOZnrSTAV"}
 
-      data: {"id":"chatcmpl-CVzA7h3O81W7Kcav9cNqmaQrk7Pio","object":"chat.completion.chunk","created":1761739311,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" files"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Yk6ndaqjh4"}
+      data: {"id":"chatcmpl-CWhZJC0z3DUeuu1TL3VBI1UmbDPr2","object":"chat.completion.chunk","created":1761910009,"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":"s9vV5Zt6Af"}
 
-      data: {"id":"chatcmpl-CVzA7h3O81W7Kcav9cNqmaQrk7Pio","object":"chat.completion.chunk","created":1761739311,"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":"K4garAYdiS"}
-
-      data: {"id":"chatcmpl-CVzA7h3O81W7Kcav9cNqmaQrk7Pio","object":"chat.completion.chunk","created":1761739311,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":154,"completion_tokens":10,"total_tokens":164,"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":"eTseDtFteVPsV"}
+      data: {"id":"chatcmpl-CWhZJC0z3DUeuu1TL3VBI1UmbDPr2","object":"chat.completion.chunk","created":1761910009,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":154,"completion_tokens":9,"total_tokens":163,"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":"aZRwb8LKGbbiX9"}
 
       data: [DONE]
 
@@ -57,15 +55,15 @@ interactions:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 629.818375ms
+    duration: 515.978334ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44484
+    content_length: 45236
     host: ""

internal/agent/testdata/TestCoderAgent/openai-gpt-5/read_a_file.yaml 🔗

@@ -24,19 +24,17 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"chatcmpl-CVz7RYyWicHAaNOUrX5BVio64NXIk","object":"chat.completion.chunk","created":1761739145,"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":"NHRD5WcbQjF9Kv"}
+      data: {"id":"chatcmpl-CWhWHAhp1KAuQiNQHtVYJLkaqqgii","object":"chat.completion.chunk","created":1761909821,"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":"KwiQzorjHP4yXF"}
 
-      data: {"id":"chatcmpl-CVz7RYyWicHAaNOUrX5BVio64NXIk","object":"chat.completion.chunk","created":1761739145,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Understanding"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"AfS"}
+      data: {"id":"chatcmpl-CWhWHAhp1KAuQiNQHtVYJLkaqqgii","object":"chat.completion.chunk","created":1761909821,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Understanding"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"6p7"}
 
-      data: {"id":"chatcmpl-CVz7RYyWicHAaNOUrX5BVio64NXIk","object":"chat.completion.chunk","created":1761739145,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Go"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"2FyAfb5Cut7hl"}
+      data: {"id":"chatcmpl-CWhWHAhp1KAuQiNQHtVYJLkaqqgii","object":"chat.completion.chunk","created":1761909821,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Go"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"idrszYUBJu5Xh"}
 
-      data: {"id":"chatcmpl-CVz7RYyWicHAaNOUrX5BVio64NXIk","object":"chat.completion.chunk","created":1761739145,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Mod"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"AOvLcw6QL6u6"}
+      data: {"id":"chatcmpl-CWhWHAhp1KAuQiNQHtVYJLkaqqgii","object":"chat.completion.chunk","created":1761909821,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Modules"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"yqcihFlL"}
 
-      data: {"id":"chatcmpl-CVz7RYyWicHAaNOUrX5BVio64NXIk","object":"chat.completion.chunk","created":1761739145,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Files"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"bYx0QbUXIC"}
+      data: {"id":"chatcmpl-CWhWHAhp1KAuQiNQHtVYJLkaqqgii","object":"chat.completion.chunk","created":1761909821,"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":"932OjQNMI8"}
 
-      data: {"id":"chatcmpl-CVz7RYyWicHAaNOUrX5BVio64NXIk","object":"chat.completion.chunk","created":1761739145,"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":"8AZHBANsEj"}
-
-      data: {"id":"chatcmpl-CVz7RYyWicHAaNOUrX5BVio64NXIk","object":"chat.completion.chunk","created":1761739145,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":129,"completion_tokens":4,"total_tokens":133,"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":"YZdL8KWfASsgF3"}
+      data: {"id":"chatcmpl-CWhWHAhp1KAuQiNQHtVYJLkaqqgii","object":"chat.completion.chunk","created":1761909821,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":129,"completion_tokens":3,"total_tokens":132,"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":"u4zstHSs1oMWL5"}
 
       data: [DONE]
 
@@ -45,15 +43,15 @@ interactions:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 1.260231917s
+    duration: 1.136475792s
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44357
+    content_length: 45109
     host: ""

internal/agent/testdata/TestCoderAgent/openai-gpt-5/simple_test.yaml 🔗

@@ -24,15 +24,13 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"chatcmpl-CVz7MIPSHlXphCyxpyRBWksQLSvTb","object":"chat.completion.chunk","created":1761739140,"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":"1vowxVSXtxJ2d4"}
+      data: {"id":"chatcmpl-CWhWAKSjJG7aS3VQh2aSTFIcnAoGw","object":"chat.completion.chunk","created":1761909814,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_65564d8ba5","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"kJ82cmYumWTGLC"}
 
-      data: {"id":"chatcmpl-CVz7MIPSHlXphCyxpyRBWksQLSvTb","object":"chat.completion.chunk","created":1761739140,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Greeting"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"KJBxcZr3"}
+      data: {"id":"chatcmpl-CWhWAKSjJG7aS3VQh2aSTFIcnAoGw","object":"chat.completion.chunk","created":1761909814,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_65564d8ba5","choices":[{"index":0,"delta":{"content":"Greetings"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"jM0L3i7"}
 
-      data: {"id":"chatcmpl-CVz7MIPSHlXphCyxpyRBWksQLSvTb","object":"chat.completion.chunk","created":1761739140,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Message"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"2ChAiMG4"}
+      data: {"id":"chatcmpl-CWhWAKSjJG7aS3VQh2aSTFIcnAoGw","object":"chat.completion.chunk","created":1761909814,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_65564d8ba5","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"BcuY68lDzu"}
 
-      data: {"id":"chatcmpl-CVz7MIPSHlXphCyxpyRBWksQLSvTb","object":"chat.completion.chunk","created":1761739140,"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":"UbBkoFM6re"}
-
-      data: {"id":"chatcmpl-CVz7MIPSHlXphCyxpyRBWksQLSvTb","object":"chat.completion.chunk","created":1761739140,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":126,"completion_tokens":2,"total_tokens":128,"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":"vPzxvpR8ryY52U"}
+      data: {"id":"chatcmpl-CWhWAKSjJG7aS3VQh2aSTFIcnAoGw","object":"chat.completion.chunk","created":1761909814,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_65564d8ba5","choices":[],"usage":{"prompt_tokens":126,"completion_tokens":1,"total_tokens":127,"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":"u4EbJXDAHkMaSU"}
 
       data: [DONE]
 
@@ -41,15 +39,15 @@ interactions:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 859.302541ms
+    duration: 1.241931458s
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44347
+    content_length: 45099
     host: ""

internal/agent/testdata/TestCoderAgent/openai-gpt-5/sourcegraph_tool.yaml 🔗

@@ -24,31 +24,35 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"chatcmpl-CVz9NVZBhmFDLAKKev5QLaOCWUO9O","object":"chat.completion.chunk","created":1761739265,"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":"fe0CpOYZlazOH5"}
+      data: {"id":"chatcmpl-CWhYTIiw1PNfl5a13JwCrw4pIultS","object":"chat.completion.chunk","created":1761909957,"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":"KK8aYMSSiDELdH"}
 
-      data: {"id":"chatcmpl-CVz9NVZBhmFDLAKKev5QLaOCWUO9O","object":"chat.completion.chunk","created":1761739265,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Searching"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"niEOlyZ"}
+      data: {"id":"chatcmpl-CWhYTIiw1PNfl5a13JwCrw4pIultS","object":"chat.completion.chunk","created":1761909957,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Searching"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"mLGYZIS"}
 
-      data: {"id":"chatcmpl-CVz9NVZBhmFDLAKKev5QLaOCWUO9O","object":"chat.completion.chunk","created":1761739265,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" for"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"u6E8aJR0iMA3"}
+      data: {"id":"chatcmpl-CWhYTIiw1PNfl5a13JwCrw4pIultS","object":"chat.completion.chunk","created":1761909957,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" '"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"2cbsMo2svk1BEP"}
 
-      data: {"id":"chatcmpl-CVz9NVZBhmFDLAKKev5QLaOCWUO9O","object":"chat.completion.chunk","created":1761739265,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" '"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"t0LrHYO84mGtQV"}
+      data: {"id":"chatcmpl-CWhYTIiw1PNfl5a13JwCrw4pIultS","object":"chat.completion.chunk","created":1761909957,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"func"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"fV3z1rB74yfO"}
 
-      data: {"id":"chatcmpl-CVz9NVZBhmFDLAKKev5QLaOCWUO9O","object":"chat.completion.chunk","created":1761739265,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"func"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"GAQWvwLXiX3M"}
+      data: {"id":"chatcmpl-CWhYTIiw1PNfl5a13JwCrw4pIultS","object":"chat.completion.chunk","created":1761909957,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" main"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"kHnjkHWN7YP"}
 
-      data: {"id":"chatcmpl-CVz9NVZBhmFDLAKKev5QLaOCWUO9O","object":"chat.completion.chunk","created":1761739265,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" main"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"jfdD4Xk3zVv"}
+      data: {"id":"chatcmpl-CWhYTIiw1PNfl5a13JwCrw4pIultS","object":"chat.completion.chunk","created":1761909957,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"'"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"vDM2T7mSIPdYRY7"}
 
-      data: {"id":"chatcmpl-CVz9NVZBhmFDLAKKev5QLaOCWUO9O","object":"chat.completion.chunk","created":1761739265,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"'"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"x1s9HJnIiJmigtW"}
+      data: {"id":"chatcmpl-CWhYTIiw1PNfl5a13JwCrw4pIultS","object":"chat.completion.chunk","created":1761909957,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"4A8UqCAKl3ulb"}
 
-      data: {"id":"chatcmpl-CVz9NVZBhmFDLAKKev5QLaOCWUO9O","object":"chat.completion.chunk","created":1761739265,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"AVSl72ugYDvrX"}
+      data: {"id":"chatcmpl-CWhYTIiw1PNfl5a13JwCrw4pIultS","object":"chat.completion.chunk","created":1761909957,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Go"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"epXxfMkv3UACx"}
 
-      data: {"id":"chatcmpl-CVz9NVZBhmFDLAKKev5QLaOCWUO9O","object":"chat.completion.chunk","created":1761739265,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Go"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"C79tD3LVf9TJB"}
+      data: {"id":"chatcmpl-CWhYTIiw1PNfl5a13JwCrw4pIultS","object":"chat.completion.chunk","created":1761909957,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Re"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"uXHKW1MHl43nQ"}
 
-      data: {"id":"chatcmpl-CVz9NVZBhmFDLAKKev5QLaOCWUO9O","object":"chat.completion.chunk","created":1761739265,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Re"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Lnc2wuAP1DFi2"}
+      data: {"id":"chatcmpl-CWhYTIiw1PNfl5a13JwCrw4pIultS","object":"chat.completion.chunk","created":1761909957,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"positories"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"94vXSJ"}
 
-      data: {"id":"chatcmpl-CVz9NVZBhmFDLAKKev5QLaOCWUO9O","object":"chat.completion.chunk","created":1761739265,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"positories"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"NeTuNV"}
+      data: {"id":"chatcmpl-CWhYTIiw1PNfl5a13JwCrw4pIultS","object":"chat.completion.chunk","created":1761909957,"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":"iiXaNVpsfub"}
 
-      data: {"id":"chatcmpl-CVz9NVZBhmFDLAKKev5QLaOCWUO9O","object":"chat.completion.chunk","created":1761739265,"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":"QEVxkLwgD4"}
+      data: {"id":"chatcmpl-CWhYTIiw1PNfl5a13JwCrw4pIultS","object":"chat.completion.chunk","created":1761909957,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Source"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"8pmzsoxNg"}
 
-      data: {"id":"chatcmpl-CVz9NVZBhmFDLAKKev5QLaOCWUO9O","object":"chat.completion.chunk","created":1761739265,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":138,"completion_tokens":10,"total_tokens":148,"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":"dJfT8DVIT9w0K"}
+      data: {"id":"chatcmpl-CWhYTIiw1PNfl5a13JwCrw4pIultS","object":"chat.completion.chunk","created":1761909957,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"graph"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Zf9Wv78IXa9"}
+
+      data: {"id":"chatcmpl-CWhYTIiw1PNfl5a13JwCrw4pIultS","object":"chat.completion.chunk","created":1761909957,"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":"LzKLlUWW2m"}
+
+      data: {"id":"chatcmpl-CWhYTIiw1PNfl5a13JwCrw4pIultS","object":"chat.completion.chunk","created":1761909957,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":138,"completion_tokens":12,"total_tokens":150,"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":"lva2Gdhcco8Ri"}
 
       data: [DONE]
 
@@ -57,15 +61,15 @@ interactions:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 448.444542ms
+    duration: 845.432125ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44407
+    content_length: 45159
     host: ""

internal/agent/testdata/TestCoderAgent/openai-gpt-5/update_a_file.yaml 🔗

@@ -24,25 +24,31 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"chatcmpl-CVz7cLwcqlyipViB3KQ0SHCuvoCcD","object":"chat.completion.chunk","created":1761739156,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_65564d8ba5","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"X6UJgdERVeZftx"}
+      data: {"id":"chatcmpl-CWhWUw6avqGtWuzPB9lUBctmnnIuj","object":"chat.completion.chunk","created":1761909834,"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":"PLDLeNZd82YM71"}
 
-      data: {"id":"chatcmpl-CVz7cLwcqlyipViB3KQ0SHCuvoCcD","object":"chat.completion.chunk","created":1761739156,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_65564d8ba5","choices":[{"index":0,"delta":{"content":"Update"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"0mP6avxjgD"}
+      data: {"id":"chatcmpl-CWhWUw6avqGtWuzPB9lUBctmnnIuj","object":"chat.completion.chunk","created":1761909834,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Main"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"oNiQ5GCygP3G"}
 
-      data: {"id":"chatcmpl-CVz7cLwcqlyipViB3KQ0SHCuvoCcD","object":"chat.completion.chunk","created":1761739156,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_65564d8ba5","choices":[{"index":0,"delta":{"content":" Print"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"6uSgVowxbg"}
+      data: {"id":"chatcmpl-CWhWUw6avqGtWuzPB9lUBctmnnIuj","object":"chat.completion.chunk","created":1761909834,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":".go"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"LFVFeDtgmVuLq"}
 
-      data: {"id":"chatcmpl-CVz7cLwcqlyipViB3KQ0SHCuvoCcD","object":"chat.completion.chunk","created":1761739156,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_65564d8ba5","choices":[{"index":0,"delta":{"content":" Statement"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"J6rKIU"}
+      data: {"id":"chatcmpl-CWhWUw6avqGtWuzPB9lUBctmnnIuj","object":"chat.completion.chunk","created":1761909834,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Update"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"t0AlYBr9y"}
 
-      data: {"id":"chatcmpl-CVz7cLwcqlyipViB3KQ0SHCuvoCcD","object":"chat.completion.chunk","created":1761739156,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_65564d8ba5","choices":[{"index":0,"delta":{"content":" in"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"HUKnpRUoNzTaa"}
+      data: {"id":"chatcmpl-CWhWUw6avqGtWuzPB9lUBctmnnIuj","object":"chat.completion.chunk","created":1761909834,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":":"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"c9qMSV1Y8ekFTIo"}
 
-      data: {"id":"chatcmpl-CVz7cLwcqlyipViB3KQ0SHCuvoCcD","object":"chat.completion.chunk","created":1761739156,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_65564d8ba5","choices":[{"index":0,"delta":{"content":" main"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"5XXcdcHGSpW"}
+      data: {"id":"chatcmpl-CWhWUw6avqGtWuzPB9lUBctmnnIuj","object":"chat.completion.chunk","created":1761909834,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Print"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"QcJdYyzl3Z"}
 
-      data: {"id":"chatcmpl-CVz7cLwcqlyipViB3KQ0SHCuvoCcD","object":"chat.completion.chunk","created":1761739156,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_65564d8ba5","choices":[{"index":0,"delta":{"content":".go"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Lw0ACzS7NeDnv"}
+      data: {"id":"chatcmpl-CWhWUw6avqGtWuzPB9lUBctmnnIuj","object":"chat.completion.chunk","created":1761909834,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" \""},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"js7cOcHrxK7UT"}
 
-      data: {"id":"chatcmpl-CVz7cLwcqlyipViB3KQ0SHCuvoCcD","object":"chat.completion.chunk","created":1761739156,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_65564d8ba5","choices":[{"index":0,"delta":{"content":" File"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"cK7347ocgYS"}
+      data: {"id":"chatcmpl-CWhWUw6avqGtWuzPB9lUBctmnnIuj","object":"chat.completion.chunk","created":1761909834,"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":"UOHtjq3TQlC"}
 
-      data: {"id":"chatcmpl-CVz7cLwcqlyipViB3KQ0SHCuvoCcD","object":"chat.completion.chunk","created":1761739156,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_65564d8ba5","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"57cdGQmo0R"}
+      data: {"id":"chatcmpl-CWhWUw6avqGtWuzPB9lUBctmnnIuj","object":"chat.completion.chunk","created":1761909834,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" from"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"36Gr5Flq5LJ"}
 
-      data: {"id":"chatcmpl-CVz7cLwcqlyipViB3KQ0SHCuvoCcD","object":"chat.completion.chunk","created":1761739156,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_65564d8ba5","choices":[],"usage":{"prompt_tokens":139,"completion_tokens":7,"total_tokens":146,"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":"ScSeP3iMu5yYl2"}
+      data: {"id":"chatcmpl-CWhWUw6avqGtWuzPB9lUBctmnnIuj","object":"chat.completion.chunk","created":1761909834,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Crush"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"fYBXSUWUd9"}
+
+      data: {"id":"chatcmpl-CWhWUw6avqGtWuzPB9lUBctmnnIuj","object":"chat.completion.chunk","created":1761909834,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"\""},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"QV42kvlcA9ibpc"}
+
+      data: {"id":"chatcmpl-CWhWUw6avqGtWuzPB9lUBctmnnIuj","object":"chat.completion.chunk","created":1761909834,"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":"yWaQRolC0O"}
+
+      data: {"id":"chatcmpl-CWhWUw6avqGtWuzPB9lUBctmnnIuj","object":"chat.completion.chunk","created":1761909834,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":139,"completion_tokens":10,"total_tokens":149,"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":"hw9iNRpmFNweD"}
 
       data: [DONE]
 
@@ -51,15 +57,15 @@ interactions:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 485.046167ms
+    duration: 469.220667ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44413
+    content_length: 45165
     host: ""

internal/agent/testdata/TestCoderAgent/openai-gpt-5/write_tool.yaml 🔗

@@ -24,23 +24,23 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"chatcmpl-CVz9pqBh43nAYUGjA4HmpNmfVpUvH","object":"chat.completion.chunk","created":1761739293,"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":"yvRftXRjka0BLl"}
+      data: {"id":"chatcmpl-CWhZ84hPjwruz42MM9mMzNrU0xGRh","object":"chat.completion.chunk","created":1761909998,"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":"6nknPjuOLCwz2j"}
 
-      data: {"id":"chatcmpl-CVz9pqBh43nAYUGjA4HmpNmfVpUvH","object":"chat.completion.chunk","created":1761739293,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Create"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"fZcBPhRtnG"}
+      data: {"id":"chatcmpl-CWhZ84hPjwruz42MM9mMzNrU0xGRh","object":"chat.completion.chunk","created":1761909998,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Create"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"UKbO0tOEx5"}
 
-      data: {"id":"chatcmpl-CVz9pqBh43nAYUGjA4HmpNmfVpUvH","object":"chat.completion.chunk","created":1761739293,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" config"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"UYN60fFyW"}
+      data: {"id":"chatcmpl-CWhZ84hPjwruz42MM9mMzNrU0xGRh","object":"chat.completion.chunk","created":1761909998,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" JSON"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Zw7na2EwlSz"}
 
-      data: {"id":"chatcmpl-CVz9pqBh43nAYUGjA4HmpNmfVpUvH","object":"chat.completion.chunk","created":1761739293,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":".json"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"arJ5vFKUof9"}
+      data: {"id":"chatcmpl-CWhZ84hPjwruz42MM9mMzNrU0xGRh","object":"chat.completion.chunk","created":1761909998,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" File"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"a1Ti8eDKQqT"}
 
-      data: {"id":"chatcmpl-CVz9pqBh43nAYUGjA4HmpNmfVpUvH","object":"chat.completion.chunk","created":1761739293,"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":"jkl1Cyas5JJ"}
+      data: {"id":"chatcmpl-CWhZ84hPjwruz42MM9mMzNrU0xGRh","object":"chat.completion.chunk","created":1761909998,"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":"rRhBI7hB4p6"}
 
-      data: {"id":"chatcmpl-CVz9pqBh43nAYUGjA4HmpNmfVpUvH","object":"chat.completion.chunk","created":1761739293,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" JSON"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"YNOesLqjiP7"}
+      data: {"id":"chatcmpl-CWhZ84hPjwruz42MM9mMzNrU0xGRh","object":"chat.completion.chunk","created":1761909998,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Config"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"VBjObVGZe"}
 
-      data: {"id":"chatcmpl-CVz9pqBh43nAYUGjA4HmpNmfVpUvH","object":"chat.completion.chunk","created":1761739293,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Content"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"tyZHLJ74"}
+      data: {"id":"chatcmpl-CWhZ84hPjwruz42MM9mMzNrU0xGRh","object":"chat.completion.chunk","created":1761909998,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":" Details"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"sbKPiIhw"}
 
-      data: {"id":"chatcmpl-CVz9pqBh43nAYUGjA4HmpNmfVpUvH","object":"chat.completion.chunk","created":1761739293,"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":"XgpssLlNl0"}
+      data: {"id":"chatcmpl-CWhZ84hPjwruz42MM9mMzNrU0xGRh","object":"chat.completion.chunk","created":1761909998,"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":"CgNioou140"}
 
-      data: {"id":"chatcmpl-CVz9pqBh43nAYUGjA4HmpNmfVpUvH","object":"chat.completion.chunk","created":1761739293,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":153,"completion_tokens":6,"total_tokens":159,"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":"ElEHEb9B9kNRsi"}
+      data: {"id":"chatcmpl-CWhZ84hPjwruz42MM9mMzNrU0xGRh","object":"chat.completion.chunk","created":1761909998,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":153,"completion_tokens":6,"total_tokens":159,"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":"1GFTG66Q9TDJFr"}
 
       data: [DONE]
 
@@ -49,15 +49,15 @@ interactions:
       - text/event-stream; charset=utf-8
     status: 200 OK
     code: 200
-    duration: 504.447625ms
+    duration: 495.299125ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44450
+    content_length: 45202
     host: ""

internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/bash_tool.yaml 🔗

@@ -24,21 +24,23 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"gen-1761739340-MhErwSz7AYBvQhO2456S","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739340,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910038-dOJ1BpSmQV2Bjlc0MYXe","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910038,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739340-MhErwSz7AYBvQhO2456S","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739340,"choices":[{"index":0,"delta":{"role":"assistant","content":"Create"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910038-dOJ1BpSmQV2Bjlc0MYXe","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910038,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739340-MhErwSz7AYBvQhO2456S","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739340,"choices":[{"index":0,"delta":{"role":"assistant","content":" test"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910038-dOJ1BpSmQV2Bjlc0MYXe","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910038,"choices":[{"index":0,"delta":{"role":"assistant","content":"Create"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739340-MhErwSz7AYBvQhO2456S","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739340,"choices":[{"index":0,"delta":{"role":"assistant","content":".txt with"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910038-dOJ1BpSmQV2Bjlc0MYXe","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910038,"choices":[{"index":0,"delta":{"role":"assistant","content":" test"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739340-MhErwSz7AYBvQhO2456S","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739340,"choices":[{"index":0,"delta":{"role":"assistant","content":" hello bash using"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910038-dOJ1BpSmQV2Bjlc0MYXe","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910038,"choices":[{"index":0,"delta":{"role":"assistant","content":".txt with"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739340-MhErwSz7AYBvQhO2456S","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739340,"choices":[{"index":0,"delta":{"role":"assistant","content":" bash"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910038-dOJ1BpSmQV2Bjlc0MYXe","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910038,"choices":[{"index":0,"delta":{"role":"assistant","content":" hello bash using"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739340-MhErwSz7AYBvQhO2456S","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739340,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
+      data: {"id":"gen-1761910038-dOJ1BpSmQV2Bjlc0MYXe","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910038,"choices":[{"index":0,"delta":{"role":"assistant","content":" bash"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739340-MhErwSz7AYBvQhO2456S","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739340,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":147,"completion_tokens":9,"total_tokens":156,"cost":0.0000468,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.0000441,"upstream_inference_completions_cost":0.0000027},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
+      data: {"id":"gen-1761910038-dOJ1BpSmQV2Bjlc0MYXe","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910038,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
+
+      data: {"id":"gen-1761910038-dOJ1BpSmQV2Bjlc0MYXe","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910038,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":147,"completion_tokens":9,"total_tokens":156,"cost":0.00003048,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.00002058,"upstream_inference_completions_cost":0.0000099},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
 
       data: [DONE]
 
@@ -47,15 +49,15 @@ interactions:
       - text/event-stream
     status: 200 OK
     code: 200
-    duration: 1.509632708s
+    duration: 305.396125ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44546
+    content_length: 45298
     host: ""

internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/download_tool.yaml 🔗

@@ -24,31 +24,25 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"gen-1761739349-jppiI9wtIfPLJgIWJYHu","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739349,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910040-jpK7NLYwz180tBpsNAIV","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910040,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739349-jppiI9wtIfPLJgIWJYHu","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739349,"choices":[{"index":0,"delta":{"role":"assistant","content":"Download"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910040-jpK7NLYwz180tBpsNAIV","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910040,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739349-jppiI9wtIfPLJgIWJYHu","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739349,"choices":[{"index":0,"delta":{"role":"assistant","content":" example"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910040-jpK7NLYwz180tBpsNAIV","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910040,"choices":[{"index":0,"delta":{"role":"assistant","content":"Download"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739349-jppiI9wtIfPLJgIWJYHu","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739349,"choices":[{"index":0,"delta":{"role":"assistant","content":".txt"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910040-jpK7NLYwz180tBpsNAIV","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910040,"choices":[{"index":0,"delta":{"role":"assistant","content":" and save example.txt"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739349-jppiI9wtIfPLJgIWJYHu","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739349,"choices":[{"index":0,"delta":{"role":"assistant","content":" from"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910040-jpK7NLYwz180tBpsNAIV","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910040,"choices":[{"index":0,"delta":{"role":"assistant","content":" from example-files"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739349-jppiI9wtIfPLJgIWJYHu","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739349,"choices":[{"index":0,"delta":{"role":"assistant","content":" example"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910040-jpK7NLYwz180tBpsNAIV","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910040,"choices":[{"index":0,"delta":{"role":"assistant","content":".online"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739349-jppiI9wtIfPLJgIWJYHu","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739349,"choices":[{"index":0,"delta":{"role":"assistant","content":"-files"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910040-jpK7NLYwz180tBpsNAIV","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910040,"choices":[{"index":0,"delta":{"role":"assistant","content":"-"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739349-jppiI9wtIfPLJgIWJYHu","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739349,"choices":[{"index":0,"delta":{"role":"assistant","content":".online"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910040-jpK7NLYwz180tBpsNAIV","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910040,"choices":[{"index":0,"delta":{"role":"assistant","content":"convert.com"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739349-jppiI9wtIfPLJgIWJYHu","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739349,"choices":[{"index":0,"delta":{"role":"assistant","content":"-"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910040-jpK7NLYwz180tBpsNAIV","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910040,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
 
-      data: {"id":"gen-1761739349-jppiI9wtIfPLJgIWJYHu","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739349,"choices":[{"index":0,"delta":{"role":"assistant","content":"convert"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
-
-      data: {"id":"gen-1761739349-jppiI9wtIfPLJgIWJYHu","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739349,"choices":[{"index":0,"delta":{"role":"assistant","content":".com"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
-
-      data: {"id":"gen-1761739349-jppiI9wtIfPLJgIWJYHu","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739349,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
-
-      data: {"id":"gen-1761739349-jppiI9wtIfPLJgIWJYHu","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739349,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":150,"completion_tokens":11,"total_tokens":161,"cost":0.0000238,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.000015,"upstream_inference_completions_cost":0.0000088},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
+      data: {"id":"gen-1761910040-jpK7NLYwz180tBpsNAIV","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910040,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":150,"completion_tokens":13,"total_tokens":163,"cost":0.0000353,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.000021,"upstream_inference_completions_cost":0.0000143},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
 
       data: [DONE]
 
@@ -57,15 +51,15 @@ interactions:
       - text/event-stream
     status: 200 OK
     code: 200
-    duration: 595.46175ms
+    duration: 710.793208ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44571
+    content_length: 45323
     host: ""

internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/fetch_tool.yaml 🔗

@@ -6,9 +6,9 @@ interactions:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 845
+    content_length: 45341
     host: ""
-    body: '{"messages":[{"content":"you will generate a short title based on the first message a user begins a conversation with\n\n<rules>\n- ensure it is not more than 50 characters long\n- the title should be a summary of the user''s message\n- it should be one line long\n- do not use quotes or colons\n- the entire text you return will be used as the title\n- never return anything that is more than one sentence (one line) long\n</rules>\n\n /no_think","role":"system"},{"content":"Generate a concise title for the following content:\n\nfetch the content from https://example-files.online-convert.com/website/html/example.html and tell me if it contains the word ''John Doe''\n <think>\n\n</think>","role":"user"}],"model":"qwen/qwen3-next-80b-a3b-instruct","max_tokens":40,"stream_options":{"include_usage":true},"usage":{"include":true},"stream":true}'

internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/glob_tool.yaml 🔗

@@ -24,19 +24,21 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"gen-1761739365-9tYIRPaRwzq4zyeyutpR","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739365,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910052-uuOETI7IJpIcw17hHFAk","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910052,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739365-9tYIRPaRwzq4zyeyutpR","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739365,"choices":[{"index":0,"delta":{"role":"assistant","content":"Find"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910052-uuOETI7IJpIcw17hHFAk","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910052,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739365-9tYIRPaRwzq4zyeyutpR","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739365,"choices":[{"index":0,"delta":{"role":"assistant","content":" all .go files"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910052-uuOETI7IJpIcw17hHFAk","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910052,"choices":[{"index":0,"delta":{"role":"assistant","content":"Find"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739365-9tYIRPaRwzq4zyeyutpR","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739365,"choices":[{"index":0,"delta":{"role":"assistant","content":" in current directory using"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910052-uuOETI7IJpIcw17hHFAk","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910052,"choices":[{"index":0,"delta":{"role":"assistant","content":" all .go files"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739365-9tYIRPaRwzq4zyeyutpR","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739365,"choices":[{"index":0,"delta":{"role":"assistant","content":" glob"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910052-uuOETI7IJpIcw17hHFAk","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910052,"choices":[{"index":0,"delta":{"role":"assistant","content":" in current directory using"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739365-9tYIRPaRwzq4zyeyutpR","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739365,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
+      data: {"id":"gen-1761910052-uuOETI7IJpIcw17hHFAk","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910052,"choices":[{"index":0,"delta":{"role":"assistant","content":" glob"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739365-9tYIRPaRwzq4zyeyutpR","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739365,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":139,"completion_tokens":11,"total_tokens":150,"cost":0.000045,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.0000417,"upstream_inference_completions_cost":0.0000033},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
+      data: {"id":"gen-1761910052-uuOETI7IJpIcw17hHFAk","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910052,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
+
+      data: {"id":"gen-1761910052-uuOETI7IJpIcw17hHFAk","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910052,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":139,"completion_tokens":11,"total_tokens":150,"cost":0.00003156,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.00001946,"upstream_inference_completions_cost":0.0000121},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
 
       data: [DONE]
 
@@ -45,15 +47,15 @@ interactions:
       - text/event-stream
     status: 200 OK
     code: 200
-    duration: 866.379167ms
+    duration: 568.273625ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44507
+    content_length: 45259
     host: ""

internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/grep_tool.yaml 🔗

@@ -24,27 +24,17 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"gen-1761739368-d3cedwgttisP1LWVaMoF","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739368,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761911360-yPPMIO7ueEGdqymanmPO","provider":"GMICloud","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761911360,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739368-d3cedwgttisP1LWVaMoF","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739368,"choices":[{"index":0,"delta":{"role":"assistant","content":"Search"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761911360-yPPMIO7ueEGdqymanmPO","provider":"GMICloud","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761911360,"choices":[{"index":0,"delta":{"role":"assistant","content":"Search"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739368-d3cedwgttisP1LWVaMoF","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739368,"choices":[{"index":0,"delta":{"role":"assistant","content":" for"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761911360-yPPMIO7ueEGdqymanmPO","provider":"GMICloud","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761911360,"choices":[{"index":0,"delta":{"role":"assistant","content":" for package in Go"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739368-d3cedwgttisP1LWVaMoF","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739368,"choices":[{"index":0,"delta":{"role":"assistant","content":" package"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761911360-yPPMIO7ueEGdqymanmPO","provider":"GMICloud","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761911360,"choices":[{"index":0,"delta":{"role":"assistant","content":" files using grep"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739368-d3cedwgttisP1LWVaMoF","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739368,"choices":[{"index":0,"delta":{"role":"assistant","content":" in"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761911360-yPPMIO7ueEGdqymanmPO","provider":"GMICloud","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761911360,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
 
-      data: {"id":"gen-1761739368-d3cedwgttisP1LWVaMoF","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739368,"choices":[{"index":0,"delta":{"role":"assistant","content":" go"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
-
-      data: {"id":"gen-1761739368-d3cedwgttisP1LWVaMoF","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739368,"choices":[{"index":0,"delta":{"role":"assistant","content":" files"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
-
-      data: {"id":"gen-1761739368-d3cedwgttisP1LWVaMoF","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739368,"choices":[{"index":0,"delta":{"role":"assistant","content":" using"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
-
-      data: {"id":"gen-1761739368-d3cedwgttisP1LWVaMoF","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739368,"choices":[{"index":0,"delta":{"role":"assistant","content":" grep"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
-
-      data: {"id":"gen-1761739368-d3cedwgttisP1LWVaMoF","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739368,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
-
-      data: {"id":"gen-1761739368-d3cedwgttisP1LWVaMoF","provider":"Chutes","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739368,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":140,"completion_tokens":9,"total_tokens":149,"cost":0.0000212,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.000014,"upstream_inference_completions_cost":0.0000072},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
+      data: {"id":"gen-1761911360-yPPMIO7ueEGdqymanmPO","provider":"GMICloud","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761911360,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":140,"completion_tokens":9,"total_tokens":149,"cost":0.0000345,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.000021,"upstream_inference_completions_cost":0.0000135},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
 
       data: [DONE]
 
@@ -53,15 +43,15 @@ interactions:
       - text/event-stream
     status: 200 OK
     code: 200
-    duration: 1.196160875s
+    duration: 2.174957041s
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44505
+    content_length: 45257
     host: ""

internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/ls_tool.yaml 🔗

@@ -24,25 +24,17 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"gen-1761739375-MQ1rVq0evwsr6N2PEE68","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739375,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910060-VJbmBjm0lLRDsPUFIR6S","provider":"Google","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910060,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739375-MQ1rVq0evwsr6N2PEE68","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739375,"choices":[{"index":0,"delta":{"role":"assistant","content":"List"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910060-VJbmBjm0lLRDsPUFIR6S","provider":"Google","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910060,"choices":[{"index":0,"delta":{"role":"assistant","content":"List"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739375-MQ1rVq0evwsr6N2PEE68","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739375,"choices":[{"index":0,"delta":{"role":"assistant","content":" files"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910060-VJbmBjm0lLRDsPUFIR6S","provider":"Google","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910060,"choices":[{"index":0,"delta":{"role":"assistant","content":" files in current directory"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739375-MQ1rVq0evwsr6N2PEE68","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739375,"choices":[{"index":0,"delta":{"role":"assistant","content":" in"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910060-VJbmBjm0lLRDsPUFIR6S","provider":"Google","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910060,"choices":[{"index":0,"delta":{"role":"assistant","content":" using ls"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739375-MQ1rVq0evwsr6N2PEE68","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739375,"choices":[{"index":0,"delta":{"role":"assistant","content":" current"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910060-VJbmBjm0lLRDsPUFIR6S","provider":"Google","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910060,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
 
-      data: {"id":"gen-1761739375-MQ1rVq0evwsr6N2PEE68","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739375,"choices":[{"index":0,"delta":{"role":"assistant","content":" directory"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
-
-      data: {"id":"gen-1761739375-MQ1rVq0evwsr6N2PEE68","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739375,"choices":[{"index":0,"delta":{"role":"assistant","content":" using"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
-
-      data: {"id":"gen-1761739375-MQ1rVq0evwsr6N2PEE68","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739375,"choices":[{"index":0,"delta":{"role":"assistant","content":" ls"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
-
-      data: {"id":"gen-1761739375-MQ1rVq0evwsr6N2PEE68","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739375,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
-
-      data: {"id":"gen-1761739375-MQ1rVq0evwsr6N2PEE68","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739375,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":137,"completion_tokens":8,"total_tokens":145,"cost":0.0000225,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.0000137,"upstream_inference_completions_cost":0.0000088},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
+      data: {"id":"gen-1761910060-VJbmBjm0lLRDsPUFIR6S","provider":"Google","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910060,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":137,"completion_tokens":8,"total_tokens":145,"cost":0.00003015,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.00002055,"upstream_inference_completions_cost":0.0000096},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
 
       data: [DONE]
 
@@ -51,15 +43,15 @@ interactions:
       - text/event-stream
     status: 200 OK
     code: 200
-    duration: 552.375417ms
+    duration: 717.847333ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44499
+    content_length: 45251
     host: ""

internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/multiedit_tool.yaml 🔗

@@ -24,21 +24,17 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"gen-1761739379-8wX8woLDnS1q1V3VLm9P","provider":"Google","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739379,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910063-ujbWJwTviThqiNBCyUac","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910063,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739379-8wX8woLDnS1q1V3VLm9P","provider":"Google","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739379,"choices":[{"index":0,"delta":{"role":"assistant","content":"Use"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910063-ujbWJwTviThqiNBCyUac","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910063,"choices":[{"index":0,"delta":{"role":"assistant","content":"Use multiedit to"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739379-8wX8woLDnS1q1V3VLm9P","provider":"Google","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739379,"choices":[{"index":0,"delta":{"role":"assistant","content":" multiedit to"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910063-ujbWJwTviThqiNBCyUac","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910063,"choices":[{"index":0,"delta":{"role":"assistant","content":" update greeting and add comment in"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739379-8wX8woLDnS1q1V3VLm9P","provider":"Google","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739379,"choices":[{"index":0,"delta":{"role":"assistant","content":" update greeting"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910063-ujbWJwTviThqiNBCyUac","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910063,"choices":[{"index":0,"delta":{"role":"assistant","content":" main.go"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739379-8wX8woLDnS1q1V3VLm9P","provider":"Google","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739379,"choices":[{"index":0,"delta":{"role":"assistant","content":" and add comment in"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910063-ujbWJwTviThqiNBCyUac","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910063,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
 
-      data: {"id":"gen-1761739379-8wX8woLDnS1q1V3VLm9P","provider":"Google","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739379,"choices":[{"index":0,"delta":{"role":"assistant","content":" main.go"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
-
-      data: {"id":"gen-1761739379-8wX8woLDnS1q1V3VLm9P","provider":"Google","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739379,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
-
-      data: {"id":"gen-1761739379-8wX8woLDnS1q1V3VLm9P","provider":"Google","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739379,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":160,"completion_tokens":14,"total_tokens":174,"cost":0.0000408,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.000024,"upstream_inference_completions_cost":0.0000168},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
+      data: {"id":"gen-1761910063-ujbWJwTviThqiNBCyUac","provider":"Hyperbolic","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910063,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":160,"completion_tokens":14,"total_tokens":174,"cost":0.0000522,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.000048,"upstream_inference_completions_cost":0.0000042},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
 
       data: [DONE]
 
@@ -47,15 +43,15 @@ interactions:
       - text/event-stream
     status: 200 OK
     code: 200
-    duration: 732.581833ms
+    duration: 1.272179209s
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44585
+    content_length: 45337
     host: ""

internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/parallel_tool_calls.yaml 🔗

@@ -24,23 +24,21 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"gen-1761739412-Y09R7myDXdUaQmaPvgvH","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739412,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910105-3A4csdIPom3cGo1LJez4","provider":"Novita","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910105,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":""}
 
-      data: {"id":"gen-1761739412-Y09R7myDXdUaQmaPvgvH","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739412,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910105-3A4csdIPom3cGo1LJez4","provider":"Novita","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910105,"choices":[{"index":0,"delta":{"role":"assistant","content":"Find"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":""}
 
-      data: {"id":"gen-1761739412-Y09R7myDXdUaQmaPvgvH","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739412,"choices":[{"index":0,"delta":{"role":"assistant","content":"Find"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910105-3A4csdIPom3cGo1LJez4","provider":"Novita","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910105,"choices":[{"index":0,"delta":{"role":"assistant","content":" .go files and"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":""}
 
-      data: {"id":"gen-1761739412-Y09R7myDXdUaQmaPvgvH","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739412,"choices":[{"index":0,"delta":{"role":"assistant","content":" .go files and"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910105-3A4csdIPom3cGo1LJez4","provider":"Novita","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910105,"choices":[{"index":0,"delta":{"role":"assistant","content":" list directory"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":""}
 
-      data: {"id":"gen-1761739412-Y09R7myDXdUaQmaPvgvH","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739412,"choices":[{"index":0,"delta":{"role":"assistant","content":" list directory"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910105-3A4csdIPom3cGo1LJez4","provider":"Novita","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910105,"choices":[{"index":0,"delta":{"role":"assistant","content":" in"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":""}
 
-      data: {"id":"gen-1761739412-Y09R7myDXdUaQmaPvgvH","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739412,"choices":[{"index":0,"delta":{"role":"assistant","content":" in"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910105-3A4csdIPom3cGo1LJez4","provider":"Novita","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910105,"choices":[{"index":0,"delta":{"role":"assistant","content":" parallel"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":""}
 
-      data: {"id":"gen-1761739412-Y09R7myDXdUaQmaPvgvH","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739412,"choices":[{"index":0,"delta":{"role":"assistant","content":" parallel"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910105-3A4csdIPom3cGo1LJez4","provider":"Novita","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910105,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}],"system_fingerprint":""}
 
-      data: {"id":"gen-1761739412-Y09R7myDXdUaQmaPvgvH","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739412,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
-
-      data: {"id":"gen-1761739412-Y09R7myDXdUaQmaPvgvH","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739412,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":156,"completion_tokens":10,"total_tokens":166,"cost":0.00003284,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.00002184,"upstream_inference_completions_cost":0.000011},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
+      data: {"id":"gen-1761910105-3A4csdIPom3cGo1LJez4","provider":"Novita","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910105,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":160,"completion_tokens":9,"total_tokens":169,"cost":0.0000375,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.000024,"upstream_inference_completions_cost":0.0000135},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
 
       data: [DONE]
 
@@ -49,15 +47,15 @@ interactions:
       - text/event-stream
     status: 200 OK
     code: 200
-    duration: 561.956875ms
+    duration: 1.345112459s
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44596
+    content_length: 45348
     host: ""

internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/simple_test.yaml 🔗

@@ -24,15 +24,13 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"gen-1761739318-IdmkJnEu9uY8yYiY9n5i","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739318,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910017-kwKmQaoZRZJprbrCiwzP","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910017,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739318-IdmkJnEu9uY8yYiY9n5i","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739318,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910017-kwKmQaoZRZJprbrCiwzP","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910017,"choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739318-IdmkJnEu9uY8yYiY9n5i","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739318,"choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910017-kwKmQaoZRZJprbrCiwzP","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910017,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
 
-      data: {"id":"gen-1761739318-IdmkJnEu9uY8yYiY9n5i","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739318,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
-
-      data: {"id":"gen-1761739318-IdmkJnEu9uY8yYiY9n5i","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739318,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":128,"completion_tokens":2,"total_tokens":130,"cost":0.00002012,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.00001792,"upstream_inference_completions_cost":0.0000022},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
+      data: {"id":"gen-1761910017-kwKmQaoZRZJprbrCiwzP","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910017,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":128,"completion_tokens":2,"total_tokens":130,"cost":0.000015,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.0000128,"upstream_inference_completions_cost":0.0000022},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
 
       data: [DONE]
 
@@ -41,15 +39,15 @@ interactions:
       - text/event-stream
     status: 200 OK
     code: 200
-    duration: 1.246189959s
+    duration: 1.304010709s
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44459
+    content_length: 45211
     host: ""

internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/sourcegraph_tool.yaml 🔗

@@ -24,31 +24,31 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"gen-1761739394-Sje6bTef73gHJt8BJ6QE","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739394,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910083-DMwj8ByxTb7hAGnhEawD","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910083,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739394-Sje6bTef73gHJt8BJ6QE","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739394,"choices":[{"index":0,"delta":{"role":"assistant","content":"Search"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910083-DMwj8ByxTb7hAGnhEawD","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910083,"choices":[{"index":0,"delta":{"role":"assistant","content":"Search"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739394-Sje6bTef73gHJt8BJ6QE","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739394,"choices":[{"index":0,"delta":{"role":"assistant","content":" for"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910083-DMwj8ByxTb7hAGnhEawD","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910083,"choices":[{"index":0,"delta":{"role":"assistant","content":" for"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739394-Sje6bTef73gHJt8BJ6QE","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739394,"choices":[{"index":0,"delta":{"role":"assistant","content":" func"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910083-DMwj8ByxTb7hAGnhEawD","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910083,"choices":[{"index":0,"delta":{"role":"assistant","content":" func"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739394-Sje6bTef73gHJt8BJ6QE","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739394,"choices":[{"index":0,"delta":{"role":"assistant","content":" main"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910083-DMwj8ByxTb7hAGnhEawD","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910083,"choices":[{"index":0,"delta":{"role":"assistant","content":" main"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739394-Sje6bTef73gHJt8BJ6QE","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739394,"choices":[{"index":0,"delta":{"role":"assistant","content":" in"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910083-DMwj8ByxTb7hAGnhEawD","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910083,"choices":[{"index":0,"delta":{"role":"assistant","content":" in"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739394-Sje6bTef73gHJt8BJ6QE","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739394,"choices":[{"index":0,"delta":{"role":"assistant","content":" Go"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910083-DMwj8ByxTb7hAGnhEawD","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910083,"choices":[{"index":0,"delta":{"role":"assistant","content":" Go"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739394-Sje6bTef73gHJt8BJ6QE","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739394,"choices":[{"index":0,"delta":{"role":"assistant","content":" repositories"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910083-DMwj8ByxTb7hAGnhEawD","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910083,"choices":[{"index":0,"delta":{"role":"assistant","content":" repositories"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739394-Sje6bTef73gHJt8BJ6QE","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739394,"choices":[{"index":0,"delta":{"role":"assistant","content":" using"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910083-DMwj8ByxTb7hAGnhEawD","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910083,"choices":[{"index":0,"delta":{"role":"assistant","content":" using"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739394-Sje6bTef73gHJt8BJ6QE","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739394,"choices":[{"index":0,"delta":{"role":"assistant","content":" Source"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910083-DMwj8ByxTb7hAGnhEawD","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910083,"choices":[{"index":0,"delta":{"role":"assistant","content":" Source"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739394-Sje6bTef73gHJt8BJ6QE","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739394,"choices":[{"index":0,"delta":{"role":"assistant","content":"graph"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910083-DMwj8ByxTb7hAGnhEawD","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910083,"choices":[{"index":0,"delta":{"role":"assistant","content":"graph"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739394-Sje6bTef73gHJt8BJ6QE","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739394,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
+      data: {"id":"gen-1761910083-DMwj8ByxTb7hAGnhEawD","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910083,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
 
-      data: {"id":"gen-1761739394-Sje6bTef73gHJt8BJ6QE","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739394,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":140,"completion_tokens":11,"total_tokens":151,"cost":0.0000261,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.000014,"upstream_inference_completions_cost":0.0000121},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
+      data: {"id":"gen-1761910083-DMwj8ByxTb7hAGnhEawD","provider":"Parasail","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910083,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":140,"completion_tokens":11,"total_tokens":151,"cost":0.0000261,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.000014,"upstream_inference_completions_cost":0.0000121},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
 
       data: [DONE]
 
@@ -57,15 +57,15 @@ interactions:
       - text/event-stream
     status: 200 OK
     code: 200
-    duration: 660.816458ms
+    duration: 781.411208ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44519
+    content_length: 45271
     host: ""

internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/update_a_file.yaml 🔗

@@ -24,19 +24,19 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"gen-1761739329-MncJd5Hu7FuIefPllGvs","provider":"Alibaba","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739329,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":null}
+      data: {"id":"gen-1761910023-cE8wcf03fPUk6DeRS4aD","provider":"Novita","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910023,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":""}
 
-      data: {"id":"gen-1761739329-MncJd5Hu7FuIefPllGvs","provider":"Alibaba","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739329,"choices":[{"index":0,"delta":{"role":"assistant","content":"Update"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":null}
+      data: {"id":"gen-1761910023-cE8wcf03fPUk6DeRS4aD","provider":"Novita","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910023,"choices":[{"index":0,"delta":{"role":"assistant","content":"Update"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":""}
 
-      data: {"id":"gen-1761739329-MncJd5Hu7FuIefPllGvs","provider":"Alibaba","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739329,"choices":[{"index":0,"delta":{"role":"assistant","content":" main.go to print"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":null}
+      data: {"id":"gen-1761910023-cE8wcf03fPUk6DeRS4aD","provider":"Novita","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910023,"choices":[{"index":0,"delta":{"role":"assistant","content":" main.go to print"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":""}
 
-      data: {"id":"gen-1761739329-MncJd5Hu7FuIefPllGvs","provider":"Alibaba","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739329,"choices":[{"index":0,"delta":{"role":"assistant","content":" hello"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":null}
+      data: {"id":"gen-1761910023-cE8wcf03fPUk6DeRS4aD","provider":"Novita","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910023,"choices":[{"index":0,"delta":{"role":"assistant","content":" hello"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":""}
 
-      data: {"id":"gen-1761739329-MncJd5Hu7FuIefPllGvs","provider":"Alibaba","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739329,"choices":[{"index":0,"delta":{"role":"assistant","content":" from crush"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":null}
+      data: {"id":"gen-1761910023-cE8wcf03fPUk6DeRS4aD","provider":"Novita","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910023,"choices":[{"index":0,"delta":{"role":"assistant","content":" from crush"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"system_fingerprint":""}
 
-      data: {"id":"gen-1761739329-MncJd5Hu7FuIefPllGvs","provider":"Alibaba","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739329,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}],"system_fingerprint":null}
+      data: {"id":"gen-1761910023-cE8wcf03fPUk6DeRS4aD","provider":"Novita","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910023,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}],"system_fingerprint":""}
 
-      data: {"id":"gen-1761739329-MncJd5Hu7FuIefPllGvs","provider":"Alibaba","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739329,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":145,"completion_tokens":8,"total_tokens":153,"cost":0.0000885,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.0000725,"upstream_inference_completions_cost":0.000016},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
+      data: {"id":"gen-1761910023-cE8wcf03fPUk6DeRS4aD","provider":"Novita","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910023,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":145,"completion_tokens":8,"total_tokens":153,"cost":0.00003375,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.00002175,"upstream_inference_completions_cost":0.000012},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
 
       data: [DONE]
 
@@ -45,15 +45,15 @@ interactions:
       - text/event-stream
     status: 200 OK
     code: 200
-    duration: 1.927088584s
+    duration: 1.368202375s
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44525
+    content_length: 45277
     host: ""

internal/agent/testdata/TestCoderAgent/openrouter-kimi-k2/write_tool.yaml 🔗

@@ -24,19 +24,17 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"gen-1761739409-Xr5oIGvyjjbROoGHaNFi","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739409,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910094-GtEfRGOYinkHCLBjmD3s","provider":"GMICloud","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910094,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739409-Xr5oIGvyjjbROoGHaNFi","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739409,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910094-GtEfRGOYinkHCLBjmD3s","provider":"GMICloud","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910094,"choices":[{"index":0,"delta":{"role":"assistant","content":"Create"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739409-Xr5oIGvyjjbROoGHaNFi","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739409,"choices":[{"index":0,"delta":{"role":"assistant","content":"Create"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910094-GtEfRGOYinkHCLBjmD3s","provider":"GMICloud","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910094,"choices":[{"index":0,"delta":{"role":"assistant","content":" config.json with name"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739409-Xr5oIGvyjjbROoGHaNFi","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739409,"choices":[{"index":0,"delta":{"role":"assistant","content":" config.json with name"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910094-GtEfRGOYinkHCLBjmD3s","provider":"GMICloud","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910094,"choices":[{"index":0,"delta":{"role":"assistant","content":" and version values"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
 
-      data: {"id":"gen-1761739409-Xr5oIGvyjjbROoGHaNFi","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739409,"choices":[{"index":0,"delta":{"role":"assistant","content":" and version data"},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
+      data: {"id":"gen-1761910094-GtEfRGOYinkHCLBjmD3s","provider":"GMICloud","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910094,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
 
-      data: {"id":"gen-1761739409-Xr5oIGvyjjbROoGHaNFi","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739409,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop","native_finish_reason":"stop","logprobs":null}]}
-
-      data: {"id":"gen-1761739409-Xr5oIGvyjjbROoGHaNFi","provider":"DeepInfra","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761739409,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":155,"completion_tokens":9,"total_tokens":164,"cost":0.0000316,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.0000217,"upstream_inference_completions_cost":0.0000099},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
+      data: {"id":"gen-1761910094-GtEfRGOYinkHCLBjmD3s","provider":"GMICloud","model":"qwen/qwen3-next-80b-a3b-instruct","object":"chat.completion.chunk","created":1761910094,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null,"native_finish_reason":null,"logprobs":null}],"usage":{"prompt_tokens":155,"completion_tokens":9,"total_tokens":164,"cost":0.00003675,"is_byok":false,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"cost_details":{"upstream_inference_cost":null,"upstream_inference_prompt_cost":0.00002325,"upstream_inference_completions_cost":0.0000135},"completion_tokens_details":{"reasoning_tokens":0,"image_tokens":0}}}
 
       data: [DONE]
 
@@ -45,15 +43,15 @@ interactions:
       - text/event-stream
     status: 200 OK
     code: 200
-    duration: 249.539125ms
+    duration: 1.740511708s
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44562
+    content_length: 45314
     host: ""

internal/agent/testdata/TestCoderAgent/zai-glm4.6/bash_tool.yaml 🔗

@@ -24,21 +24,21 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"20251029200356894bd1a4c4464c46","created":1761739436,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
+      data: {"id":"202510311928475a9131ca38d64af3","created":1761910127,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
 
-      data: {"id":"20251029200356894bd1a4c4464c46","created":1761739436,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"Create"}}]}
+      data: {"id":"202510311928475a9131ca38d64af3","created":1761910127,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"B"}}]}
 
-      data: {"id":"20251029200356894bd1a4c4464c46","created":1761739436,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" test"}}]}
+      data: {"id":"202510311928475a9131ca38d64af3","created":1761910127,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"ash"}}]}
 
-      data: {"id":"20251029200356894bd1a4c4464c46","created":1761739436,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":".txt"}}]}
+      data: {"id":"202510311928475a9131ca38d64af3","created":1761910127,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" file"}}]}
 
-      data: {"id":"20251029200356894bd1a4c4464c46","created":1761739436,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" with"}}]}
+      data: {"id":"202510311928475a9131ca38d64af3","created":1761910127,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" creation"}}]}
 
-      data: {"id":"20251029200356894bd1a4c4464c46","created":1761739436,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" hello"}}]}
+      data: {"id":"202510311928475a9131ca38d64af3","created":1761910127,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" without"}}]}
 
-      data: {"id":"20251029200356894bd1a4c4464c46","created":1761739436,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" bash"}}]}
+      data: {"id":"202510311928475a9131ca38d64af3","created":1761910127,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" timestamp"}}]}
 
-      data: {"id":"20251029200356894bd1a4c4464c46","created":1761739436,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":140,"completion_tokens":10,"total_tokens":150,"prompt_tokens_details":{"cached_tokens":21}}}
+      data: {"id":"202510311928475a9131ca38d64af3","created":1761910127,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":140,"completion_tokens":10,"total_tokens":150,"prompt_tokens_details":{"cached_tokens":114}}}
 
       data: [DONE]
 
@@ -47,15 +47,15 @@ interactions:
       - text/event-stream;charset=UTF-8
     status: 200 OK
     code: 200
-    duration: 740.930958ms
+    duration: 721.892875ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44423
+    content_length: 45175
     host: ""

internal/agent/testdata/TestCoderAgent/zai-glm4.6/download_tool.yaml 🔗

@@ -24,19 +24,23 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"2025102920040009471dbb3c37434e","created":1761739440,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
+      data: {"id":"202510311928525f9092beb5134db5","created":1761910133,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
 
-      data: {"id":"2025102920040009471dbb3c37434e","created":1761739440,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"Download"}}]}
+      data: {"id":"202510311928525f9092beb5134db5","created":1761910133,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"Download"}}]}
 
-      data: {"id":"2025102920040009471dbb3c37434e","created":1761739440,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" example"}}]}
+      data: {"id":"202510311928525f9092beb5134db5","created":1761910133,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" file"}}]}
 
-      data: {"id":"2025102920040009471dbb3c37434e","created":1761739440,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":".txt"}}]}
+      data: {"id":"202510311928525f9092beb5134db5","created":1761910133,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" and"}}]}
 
-      data: {"id":"2025102920040009471dbb3c37434e","created":1761739440,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" from"}}]}
+      data: {"id":"202510311928525f9092beb5134db5","created":1761910133,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" save"}}]}
 
-      data: {"id":"2025102920040009471dbb3c37434e","created":1761739440,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" URL"}}]}
+      data: {"id":"202510311928525f9092beb5134db5","created":1761910133,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" as"}}]}
 
-      data: {"id":"2025102920040009471dbb3c37434e","created":1761739440,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":143,"completion_tokens":9,"total_tokens":152,"prompt_tokens_details":{"cached_tokens":114}}}
+      data: {"id":"202510311928525f9092beb5134db5","created":1761910133,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" example"}}]}
+
+      data: {"id":"202510311928525f9092beb5134db5","created":1761910133,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":".txt"}}]}
+
+      data: {"id":"202510311928525f9092beb5134db5","created":1761910133,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":143,"completion_tokens":11,"total_tokens":154,"prompt_tokens_details":{"cached_tokens":114}}}
 
       data: [DONE]
 
@@ -45,15 +49,15 @@ interactions:
       - text/event-stream;charset=UTF-8
     status: 200 OK
     code: 200
-    duration: 743.129ms
+    duration: 985.514791ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44448
+    content_length: 45200
     host: ""

internal/agent/testdata/TestCoderAgent/zai-glm4.6/fetch_tool.yaml 🔗

@@ -6,9 +6,9 @@ interactions:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 799
+    content_length: 45218
     host: ""
-    body: '{"messages":[{"content":"you will generate a short title based on the first message a user begins a conversation with\n\n<rules>\n- ensure it is not more than 50 characters long\n- the title should be a summary of the user''s message\n- it should be one line long\n- do not use quotes or colons\n- the entire text you return will be used as the title\n- never return anything that is more than one sentence (one line) long\n</rules>\n\n /no_think","role":"system"},{"content":"Generate a concise title for the following content:\n\nfetch the content from https://example-files.online-convert.com/website/html/example.html and tell me if it contains the word ''John Doe''\n <think>\n\n</think>","role":"user"}],"model":"glm-4.5-air","max_tokens":40,"stream_options":{"include_usage":true},"stream":true}'

internal/agent/testdata/TestCoderAgent/zai-glm4.6/glob_tool.yaml 🔗

@@ -24,21 +24,19 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"20251029200411fd81181f53354f03","created":1761739451,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
+      data: {"id":"20251031194927a02c47d9d61349e2","created":1761911367,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
 
-      data: {"id":"20251029200411fd81181f53354f03","created":1761739451,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"Find"}}]}
+      data: {"id":"20251031194927a02c47d9d61349e2","created":1761911367,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"Find"}}]}
 
-      data: {"id":"20251029200411fd81181f53354f03","created":1761739451,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" all"}}]}
+      data: {"id":"20251031194927a02c47d9d61349e2","created":1761911367,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" Go"}}]}
 
-      data: {"id":"20251029200411fd81181f53354f03","created":1761739451,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" Go"}}]}
+      data: {"id":"20251031194927a02c47d9d61349e2","created":1761911367,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" Files"}}]}
 
-      data: {"id":"20251029200411fd81181f53354f03","created":1761739451,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" files"}}]}
+      data: {"id":"20251031194927a02c47d9d61349e2","created":1761911367,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" with"}}]}
 
-      data: {"id":"20251029200411fd81181f53354f03","created":1761739451,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" with"}}]}
+      data: {"id":"20251031194927a02c47d9d61349e2","created":1761911367,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" Glob"}}]}
 
-      data: {"id":"20251029200411fd81181f53354f03","created":1761739451,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" glob"}}]}
-
-      data: {"id":"20251029200411fd81181f53354f03","created":1761739451,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":132,"completion_tokens":10,"total_tokens":142,"prompt_tokens_details":{"cached_tokens":115}}}
+      data: {"id":"20251031194927a02c47d9d61349e2","created":1761911367,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":132,"completion_tokens":9,"total_tokens":141,"prompt_tokens_details":{"cached_tokens":122}}}
 
       data: [DONE]
 
@@ -47,15 +45,15 @@ interactions:
       - text/event-stream;charset=UTF-8
     status: 200 OK
     code: 200
-    duration: 846.707459ms
+    duration: 2.265072959s
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44384
+    content_length: 45136
     host: ""

internal/agent/testdata/TestCoderAgent/zai-glm4.6/grep_tool.yaml 🔗

@@ -24,17 +24,23 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"20251029200416d739eb7454db496d","created":1761739456,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
+      data: {"id":"202510311929138fde4a0b262745c5","created":1761910153,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
 
-      data: {"id":"20251029200416d739eb7454db496d","created":1761739456,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"grep"}}]}
+      data: {"id":"202510311929138fde4a0b262745c5","created":1761910153,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"G"}}]}
 
-      data: {"id":"20251029200416d739eb7454db496d","created":1761739456,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" package"}}]}
+      data: {"id":"202510311929138fde4a0b262745c5","created":1761910153,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"rep"}}]}
 
-      data: {"id":"20251029200416d739eb7454db496d","created":1761739456,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" go"}}]}
+      data: {"id":"202510311929138fde4a0b262745c5","created":1761910153,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" for"}}]}
 
-      data: {"id":"20251029200416d739eb7454db496d","created":1761739456,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" files"}}]}
+      data: {"id":"202510311929138fde4a0b262745c5","created":1761910153,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" package"}}]}
 
-      data: {"id":"20251029200416d739eb7454db496d","created":1761739456,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":133,"completion_tokens":8,"total_tokens":141,"prompt_tokens_details":{"cached_tokens":114}}}
+      data: {"id":"202510311929138fde4a0b262745c5","created":1761910153,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" in"}}]}
+
+      data: {"id":"202510311929138fde4a0b262745c5","created":1761910153,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" Go"}}]}
+
+      data: {"id":"202510311929138fde4a0b262745c5","created":1761910153,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" files"}}]}
+
+      data: {"id":"202510311929138fde4a0b262745c5","created":1761910153,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":133,"completion_tokens":11,"total_tokens":144,"prompt_tokens_details":{"cached_tokens":4}}}
 
       data: [DONE]
 
@@ -43,15 +49,15 @@ interactions:
       - text/event-stream;charset=UTF-8
     status: 200 OK
     code: 200
-    duration: 1.200410625s
+    duration: 674.16375ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44382
+    content_length: 45134
     host: ""

internal/agent/testdata/TestCoderAgent/zai-glm4.6/ls_tool.yaml 🔗

@@ -6,9 +6,9 @@ interactions:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 712
+    content_length: 45128
     host: ""
-    body: '{"messages":[{"content":"you will generate a short title based on the first message a user begins a conversation with\n\n<rules>\n- ensure it is not more than 50 characters long\n- the title should be a summary of the user''s message\n- it should be one line long\n- do not use quotes or colons\n- the entire text you return will be used as the title\n- never return anything that is more than one sentence (one line) long\n</rules>\n\n /no_think","role":"system"},{"content":"Generate a concise title for the following content:\n\nuse ls to list the files in the current directory\n <think>\n\n</think>","role":"user"}],"model":"glm-4.5-air","max_tokens":40,"stream_options":{"include_usage":true},"stream":true}'

internal/agent/testdata/TestCoderAgent/zai-glm4.6/multiedit_tool.yaml 🔗

@@ -24,25 +24,21 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"202510292004259eac19652f9f410e","created":1761739465,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
+      data: {"id":"20251031192917320e615cfd094ad2","created":1761910157,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
 
-      data: {"id":"202510292004259eac19652f9f410e","created":1761739465,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"Mult"}}]}
+      data: {"id":"20251031192917320e615cfd094ad2","created":1761910157,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"Mult"}}]}
 
-      data: {"id":"202510292004259eac19652f9f410e","created":1761739465,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"ied"}}]}
+      data: {"id":"20251031192917320e615cfd094ad2","created":1761910157,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"ied"}}]}
 
-      data: {"id":"202510292004259eac19652f9f410e","created":1761739465,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"it"}}]}
+      data: {"id":"20251031192917320e615cfd094ad2","created":1761910157,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"it"}}]}
 
-      data: {"id":"202510292004259eac19652f9f410e","created":1761739465,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" main"}}]}
+      data: {"id":"20251031192917320e615cfd094ad2","created":1761910157,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" Go"}}]}
 
-      data: {"id":"202510292004259eac19652f9f410e","created":1761739465,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":".go"}}]}
+      data: {"id":"20251031192917320e615cfd094ad2","created":1761910157,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" File"}}]}
 
-      data: {"id":"202510292004259eac19652f9f410e","created":1761739465,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" changes"}}]}
+      data: {"id":"20251031192917320e615cfd094ad2","created":1761910157,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" Modification"}}]}
 
-      data: {"id":"202510292004259eac19652f9f410e","created":1761739465,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" and"}}]}
-
-      data: {"id":"202510292004259eac19652f9f410e","created":1761739465,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" comment"}}]}
-
-      data: {"id":"202510292004259eac19652f9f410e","created":1761739465,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":153,"completion_tokens":12,"total_tokens":165,"prompt_tokens_details":{"cached_tokens":114}}}
+      data: {"id":"20251031192917320e615cfd094ad2","created":1761910157,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":153,"completion_tokens":10,"total_tokens":163,"prompt_tokens_details":{"cached_tokens":115}}}
 
       data: [DONE]
 
@@ -51,15 +47,15 @@ interactions:
       - text/event-stream;charset=UTF-8
     status: 200 OK
     code: 200
-    duration: 663.65375ms
+    duration: 825.229333ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44462
+    content_length: 45214
     host: ""

internal/agent/testdata/TestCoderAgent/zai-glm4.6/read_a_file.yaml 🔗

@@ -24,15 +24,17 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"2025102920034011a5ceffa9294139","created":1761739420,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
+      data: {"id":"202510311928360f6fed40ae54414a","created":1761910116,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
 
-      data: {"id":"2025102920034011a5ceffa9294139","created":1761739420,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"Go"}}]}
+      data: {"id":"202510311928360f6fed40ae54414a","created":1761910116,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"Read"}}]}
 
-      data: {"id":"2025102920034011a5ceffa9294139","created":1761739420,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" Module"}}]}
+      data: {"id":"202510311928360f6fed40ae54414a","created":1761910116,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" the"}}]}
 
-      data: {"id":"2025102920034011a5ceffa9294139","created":1761739420,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" Instructions"}}]}
+      data: {"id":"202510311928360f6fed40ae54414a","created":1761910116,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" go"}}]}
 
-      data: {"id":"2025102920034011a5ceffa9294139","created":1761739420,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":124,"completion_tokens":7,"total_tokens":131,"prompt_tokens_details":{"cached_tokens":4}}}
+      data: {"id":"202510311928360f6fed40ae54414a","created":1761910116,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" mod"}}]}
+
+      data: {"id":"202510311928360f6fed40ae54414a","created":1761910116,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":124,"completion_tokens":8,"total_tokens":132,"prompt_tokens_details":{"cached_tokens":4}}}
 
       data: [DONE]
 
@@ -41,15 +43,15 @@ interactions:
       - text/event-stream;charset=UTF-8
     status: 200 OK
     code: 200
-    duration: 937.205417ms
+    duration: 784.494417ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44346
+    content_length: 45098
     host: ""

internal/agent/testdata/TestCoderAgent/zai-glm4.6/simple_test.yaml 🔗

@@ -24,13 +24,11 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"202510292003376739b0e60ce941f9","created":1761739417,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
+      data: {"id":"20251031192833c9eb6e69b4dd4ae5","created":1761910113,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
 
-      data: {"id":"202510292003376739b0e60ce941f9","created":1761739417,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"G"}}]}
+      data: {"id":"20251031192833c9eb6e69b4dd4ae5","created":1761910113,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"}}]}
 
-      data: {"id":"202510292003376739b0e60ce941f9","created":1761739417,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"reeting"}}]}
-
-      data: {"id":"202510292003376739b0e60ce941f9","created":1761739417,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":121,"completion_tokens":6,"total_tokens":127,"prompt_tokens_details":{"cached_tokens":4}}}
+      data: {"id":"20251031192833c9eb6e69b4dd4ae5","created":1761910113,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":121,"completion_tokens":5,"total_tokens":126,"prompt_tokens_details":{"cached_tokens":4}}}
 
       data: [DONE]
 
@@ -39,15 +37,15 @@ interactions:
       - text/event-stream;charset=UTF-8
     status: 200 OK
     code: 200
-    duration: 2.487319667s
+    duration: 2.120641833s
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44336
+    content_length: 45088
     host: ""

internal/agent/testdata/TestCoderAgent/zai-glm4.6/sourcegraph_tool.yaml 🔗

@@ -6,9 +6,9 @@ interactions:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 723
+    content_length: 45148
     host: ""
-    body: '{"messages":[{"content":"you will generate a short title based on the first message a user begins a conversation with\n\n<rules>\n- ensure it is not more than 50 characters long\n- the title should be a summary of the user''s message\n- it should be one line long\n- do not use quotes or colons\n- the entire text you return will be used as the title\n- never return anything that is more than one sentence (one line) long\n</rules>\n\n /no_think","role":"system"},{"content":"Generate a concise title for the following content:\n\nuse sourcegraph to search for ''func main'' in Go repositories\n <think>\n\n</think>","role":"user"}],"model":"glm-4.5-air","max_tokens":40,"stream_options":{"include_usage":true},"stream":true}'

internal/agent/testdata/TestCoderAgent/zai-glm4.6/update_a_file.yaml 🔗

@@ -24,23 +24,21 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"202510292003484ef425e7627b4601","created":1761739428,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
+      data: {"id":"2025103119284010db94ea823c4a22","created":1761910120,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
 
-      data: {"id":"202510292003484ef425e7627b4601","created":1761739428,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"Update"}}]}
+      data: {"id":"2025103119284010db94ea823c4a22","created":1761910120,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"Update"}}]}
 
-      data: {"id":"202510292003484ef425e7627b4601","created":1761739428,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" main"}}]}
+      data: {"id":"2025103119284010db94ea823c4a22","created":1761910120,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" main"}}]}
 
-      data: {"id":"202510292003484ef425e7627b4601","created":1761739428,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":".go"}}]}
+      data: {"id":"2025103119284010db94ea823c4a22","created":1761910120,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":".go"}}]}
 
-      data: {"id":"202510292003484ef425e7627b4601","created":1761739428,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" with"}}]}
+      data: {"id":"2025103119284010db94ea823c4a22","created":1761910120,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" to"}}]}
 
-      data: {"id":"202510292003484ef425e7627b4601","created":1761739428,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" crush"}}]}
+      data: {"id":"2025103119284010db94ea823c4a22","created":1761910120,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" print"}}]}
 
-      data: {"id":"202510292003484ef425e7627b4601","created":1761739428,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" hello"}}]}
+      data: {"id":"2025103119284010db94ea823c4a22","created":1761910120,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" hello"}}]}
 
-      data: {"id":"202510292003484ef425e7627b4601","created":1761739428,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" message"}}]}
-
-      data: {"id":"202510292003484ef425e7627b4601","created":1761739428,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":134,"completion_tokens":11,"total_tokens":145,"prompt_tokens_details":{"cached_tokens":4}}}
+      data: {"id":"2025103119284010db94ea823c4a22","created":1761910120,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":134,"completion_tokens":10,"total_tokens":144,"prompt_tokens_details":{"cached_tokens":4}}}
 
       data: [DONE]
 
@@ -49,15 +47,15 @@ interactions:
       - text/event-stream;charset=UTF-8
     status: 200 OK
     code: 200
-    duration: 626.21525ms
+    duration: 1.158018209s
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44402
+    content_length: 45154
     host: ""

internal/agent/testdata/TestCoderAgent/zai-glm4.6/write_tool.yaml 🔗

@@ -24,21 +24,19 @@ interactions:
     proto_minor: 0
     content_length: -1
     body: |+
-      data: {"id":"202510292004575c0d2dedae6b4990","created":1761739497,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
+      data: {"id":"2025103119294422b16915b94e40a0","created":1761910184,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"\n"}}]}
 
-      data: {"id":"202510292004575c0d2dedae6b4990","created":1761739497,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"Create"}}]}
+      data: {"id":"2025103119294422b16915b94e40a0","created":1761910184,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":"Create"}}]}
 
-      data: {"id":"202510292004575c0d2dedae6b4990","created":1761739497,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" config"}}]}
+      data: {"id":"2025103119294422b16915b94e40a0","created":1761910184,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" config"}}]}
 
-      data: {"id":"202510292004575c0d2dedae6b4990","created":1761739497,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":".json"}}]}
+      data: {"id":"2025103119294422b16915b94e40a0","created":1761910184,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":".json"}}]}
 
-      data: {"id":"202510292004575c0d2dedae6b4990","created":1761739497,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" with"}}]}
+      data: {"id":"2025103119294422b16915b94e40a0","created":1761910184,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" with"}}]}
 
-      data: {"id":"202510292004575c0d2dedae6b4990","created":1761739497,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" write"}}]}
+      data: {"id":"2025103119294422b16915b94e40a0","created":1761910184,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" content"}}]}
 
-      data: {"id":"202510292004575c0d2dedae6b4990","created":1761739497,"model":"glm-4.5-air","choices":[{"index":0,"delta":{"role":"assistant","content":" command"}}]}
-
-      data: {"id":"202510292004575c0d2dedae6b4990","created":1761739497,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":148,"completion_tokens":10,"total_tokens":158,"prompt_tokens_details":{"cached_tokens":115}}}
+      data: {"id":"2025103119294422b16915b94e40a0","created":1761910184,"model":"glm-4.5-air","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":148,"completion_tokens":9,"total_tokens":157,"prompt_tokens_details":{"cached_tokens":4}}}
 
       data: [DONE]
 
@@ -47,15 +45,15 @@ interactions:
       - text/event-stream;charset=UTF-8
     status: 200 OK
     code: 200
-    duration: 738.070334ms
+    duration: 781.244542ms
 - id: 1
   request:
     proto: HTTP/1.1
     proto_major: 1
     proto_minor: 1
-    content_length: 44439
+    content_length: 45191
     host: ""

internal/agent/tools/fetch.go 🔗

@@ -16,24 +16,6 @@ import (
 	"github.com/charmbracelet/crush/internal/permission"
 )
 
-type FetchParams struct {
-	URL     string `json:"url" description:"The URL to fetch content from"`
-	Format  string `json:"format" description:"The format to return the content in (text, markdown, or html)"`
-	Timeout int    `json:"timeout,omitempty" description:"Optional timeout in seconds (max 120)"`
-}
-
-type FetchPermissionsParams struct {
-	URL     string `json:"url"`
-	Format  string `json:"format"`
-	Timeout int    `json:"timeout,omitempty"`
-}
-
-type fetchTool struct {
-	client      *http.Client
-	permissions permission.Service
-	workingDir  string
-}
-
 const FetchToolName = "fetch"
 
 //go:embed fetch.md

internal/agent/tools/fetch.md 🔗

@@ -1,4 +1,18 @@
-Fetches content from URL and returns it in specified format.
+Fetches raw content from URL and returns it in specified format without any AI processing.
+
+<when_to_use>
+Use this tool when you need:
+- Raw, unprocessed content from a URL
+- Direct access to API responses or JSON data
+- HTML/text/markdown content without interpretation
+- Simple, fast content retrieval without analysis
+- To save tokens by avoiding AI processing
+
+DO NOT use this tool when you need to:
+- Extract specific information from a webpage (use agentic_fetch instead)
+- Answer questions about web content (use agentic_fetch instead)
+- Analyze or summarize web pages (use agentic_fetch instead)
+</when_to_use>
 
 <usage>
 - Provide URL to fetch content from
@@ -9,6 +23,7 @@ Fetches content from URL and returns it in specified format.
 <features>
 - Supports three output formats: text, markdown, html
 - Auto-handles HTTP redirects
+- Fast and lightweight - no AI processing
 - Sets reasonable timeouts to prevent hanging
 - Validates input parameters before requests
 </features>
@@ -18,6 +33,7 @@ Fetches content from URL and returns it in specified format.
 - Only supports HTTP and HTTPS protocols
 - Cannot handle authentication or cookies
 - Some websites may block automated requests
+- Returns raw content only - no analysis or extraction
 </limitations>
 
 <tips>
@@ -25,4 +41,5 @@ Fetches content from URL and returns it in specified format.
 - Use markdown format for content that should be rendered with formatting
 - Use html format when you need raw HTML structure
 - Set appropriate timeouts for potentially slow websites
+- If the user asks to analyze or extract from a page, use agentic_fetch instead
 </tips>

internal/agent/tools/fetch_helpers.go 🔗

@@ -0,0 +1,96 @@
+package tools
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+	"unicode/utf8"
+
+	md "github.com/JohannesKaufmann/html-to-markdown"
+)
+
+// FetchURLAndConvert fetches a URL and converts HTML content to markdown.
+func FetchURLAndConvert(ctx context.Context, client *http.Client, url string) (string, error) {
+	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
+	if err != nil {
+		return "", fmt.Errorf("failed to create request: %w", err)
+	}
+
+	req.Header.Set("User-Agent", "crush/1.0")
+
+	resp, err := client.Do(req)
+	if err != nil {
+		return "", fmt.Errorf("failed to fetch URL: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return "", fmt.Errorf("request failed with status code: %d", resp.StatusCode)
+	}
+
+	maxSize := int64(5 * 1024 * 1024) // 5MB
+	body, err := io.ReadAll(io.LimitReader(resp.Body, maxSize))
+	if err != nil {
+		return "", fmt.Errorf("failed to read response body: %w", err)
+	}
+
+	content := string(body)
+
+	if !utf8.ValidString(content) {
+		return "", errors.New("response content is not valid UTF-8")
+	}
+
+	contentType := resp.Header.Get("Content-Type")
+
+	// Convert HTML to markdown for better AI processing.
+	if strings.Contains(contentType, "text/html") {
+		markdown, err := ConvertHTMLToMarkdown(content)
+		if err != nil {
+			return "", fmt.Errorf("failed to convert HTML to markdown: %w", err)
+		}
+		content = markdown
+	} else if strings.Contains(contentType, "application/json") || strings.Contains(contentType, "text/json") {
+		// Format JSON for better readability.
+		formatted, err := FormatJSON(content)
+		if err == nil {
+			content = formatted
+		}
+		// If formatting fails, keep original content.
+	}
+
+	return content, nil
+}
+
+// ConvertHTMLToMarkdown converts HTML content to markdown format.
+func ConvertHTMLToMarkdown(html string) (string, error) {
+	converter := md.NewConverter("", true, nil)
+
+	markdown, err := converter.ConvertString(html)
+	if err != nil {
+		return "", err
+	}
+
+	return markdown, nil
+}
+
+// FormatJSON formats JSON content with proper indentation.
+func FormatJSON(content string) (string, error) {
+	var data interface{}
+	if err := json.Unmarshal([]byte(content), &data); err != nil {
+		return "", err
+	}
+
+	var buf bytes.Buffer
+	encoder := json.NewEncoder(&buf)
+	encoder.SetIndent("", "  ")
+	if err := encoder.Encode(data); err != nil {
+		return "", err
+	}
+
+	return buf.String(), nil
+}

internal/agent/tools/fetch_types.go 🔗

@@ -0,0 +1,41 @@
+package tools
+
+// AgenticFetchToolName is the name of the agentic fetch tool.
+const AgenticFetchToolName = "agentic_fetch"
+
+// WebFetchToolName is the name of the web_fetch tool.
+const WebFetchToolName = "web_fetch"
+
+// LargeContentThreshold is the size threshold for saving content to a file.
+const LargeContentThreshold = 50000 // 50KB
+
+// AgenticFetchParams defines the parameters for the agentic fetch tool.
+type AgenticFetchParams struct {
+	URL    string `json:"url" description:"The URL to fetch content from"`
+	Prompt string `json:"prompt" description:"The prompt to run on the fetched content"`
+}
+
+// AgenticFetchPermissionsParams defines the permission parameters for the agentic fetch tool.
+type AgenticFetchPermissionsParams struct {
+	URL    string `json:"url"`
+	Prompt string `json:"prompt"`
+}
+
+// WebFetchParams defines the parameters for the web_fetch tool.
+type WebFetchParams struct {
+	URL string `json:"url" description:"The URL to fetch content from"`
+}
+
+// FetchParams defines the parameters for the simple fetch tool.
+type FetchParams struct {
+	URL     string `json:"url" description:"The URL to fetch content from"`
+	Format  string `json:"format" description:"The format to return the content in (text, markdown, or html)"`
+	Timeout int    `json:"timeout,omitempty" description:"Optional timeout in seconds (max 120)"`
+}
+
+// FetchPermissionsParams defines the permission parameters for the simple fetch tool.
+type FetchPermissionsParams struct {
+	URL     string `json:"url"`
+	Format  string `json:"format"`
+	Timeout int    `json:"timeout,omitempty"`
+}

internal/agent/tools/mcp-tools.go 🔗

@@ -1,94 +1,32 @@
 package tools
 
 import (
-	"cmp"
 	"context"
-	"encoding/json"
-	"errors"
 	"fmt"
-	"io"
-	"log/slog"
-	"maps"
-	"net/http"
-	"os"
-	"os/exec"
-	"slices"
-	"strings"
-	"sync"
-	"time"
 
 	"charm.land/fantasy"
-	"github.com/charmbracelet/crush/internal/config"
-	"github.com/charmbracelet/crush/internal/csync"
-	"github.com/charmbracelet/crush/internal/home"
+	"github.com/charmbracelet/crush/internal/agent/tools/mcp"
 	"github.com/charmbracelet/crush/internal/permission"
-	"github.com/charmbracelet/crush/internal/pubsub"
-	"github.com/charmbracelet/crush/internal/version"
-	"github.com/modelcontextprotocol/go-sdk/mcp"
 )
 
-// MCPState represents the current state of an MCP client
-type MCPState int
-
-const (
-	MCPStateDisabled MCPState = iota
-	MCPStateStarting
-	MCPStateConnected
-	MCPStateError
-)
-
-func (s MCPState) String() string {
-	switch s {
-	case MCPStateDisabled:
-		return "disabled"
-	case MCPStateStarting:
-		return "starting"
-	case MCPStateConnected:
-		return "connected"
-	case MCPStateError:
-		return "error"
-	default:
-		return "unknown"
+// GetMCPTools gets all the currently available MCP tools.
+func GetMCPTools(permissions permission.Service, wd string) []*Tool {
+	var result []*Tool
+	for mcpName, tools := range mcp.Tools() {
+		for _, tool := range tools {
+			result = append(result, &Tool{
+				mcpName:     mcpName,
+				tool:        tool,
+				permissions: permissions,
+				workingDir:  wd,
+			})
+		}
 	}
+	return result
 }
 
-// MCPEventType represents the type of MCP event
-type MCPEventType string
-
-const (
-	MCPEventStateChanged     MCPEventType = "state_changed"
-	MCPEventToolsListChanged MCPEventType = "tools_list_changed"
-)
-
-// MCPEvent represents an event in the MCP system
-type MCPEvent struct {
-	Type      MCPEventType
-	Name      string
-	State     MCPState
-	Error     error
-	ToolCount int
-}
-
-// MCPClientInfo holds information about an MCP client's state
-type MCPClientInfo struct {
-	Name        string
-	State       MCPState
-	Error       error
-	Client      *mcp.ClientSession
-	ToolCount   int
-	ConnectedAt time.Time
-}
-
-var (
-	mcpToolsOnce    sync.Once
-	mcpTools        = csync.NewMap[string, *McpTool]()
-	mcpClient2Tools = csync.NewMap[string, []*McpTool]()
-	mcpClients      = csync.NewMap[string, *mcp.ClientSession]()
-	mcpStates       = csync.NewMap[string, MCPClientInfo]()
-	mcpBroker       = pubsub.NewBroker[MCPEvent]()
-)
-
-type McpTool struct {
+// Tool is a tool from a MCP.
+type Tool struct {
 	mcpName         string
 	tool            *mcp.Tool
 	permissions     permission.Service
@@ -96,31 +34,31 @@ type McpTool struct {
 	providerOptions fantasy.ProviderOptions
 }
 
-func (m *McpTool) SetProviderOptions(opts fantasy.ProviderOptions) {
+func (m *Tool) SetProviderOptions(opts fantasy.ProviderOptions) {
 	m.providerOptions = opts
 }
 
-func (m *McpTool) ProviderOptions() fantasy.ProviderOptions {
+func (m *Tool) ProviderOptions() fantasy.ProviderOptions {
 	return m.providerOptions
 }
 
-func (m *McpTool) Name() string {
+func (m *Tool) Name() string {
 	return fmt.Sprintf("mcp_%s_%s", m.mcpName, m.tool.Name)
 }
 
-func (m *McpTool) MCP() string {
+func (m *Tool) MCP() string {
 	return m.mcpName
 }
 
-func (m *McpTool) MCPToolName() string {
+func (m *Tool) MCPToolName() string {
 	return m.tool.Name
 }
 
-func (b *McpTool) Info() fantasy.ToolInfo {
+func (m *Tool) Info() fantasy.ToolInfo {
 	parameters := make(map[string]any)
 	required := make([]string, 0)
 
-	if input, ok := b.tool.InputSchema.(map[string]any); ok {
+	if input, ok := m.tool.InputSchema.(map[string]any); ok {
 		if props, ok := input["properties"].(map[string]any); ok {
 			parameters = props
 		}
@@ -138,72 +76,14 @@ func (b *McpTool) Info() fantasy.ToolInfo {
 	}
 
 	return fantasy.ToolInfo{
-		Name:        fmt.Sprintf("mcp_%s_%s", b.mcpName, b.tool.Name),
-		Description: b.tool.Description,
+		Name:        m.Name(),
+		Description: m.tool.Description,
 		Parameters:  parameters,
 		Required:    required,
 	}
 }
 
-func runTool(ctx context.Context, name, toolName string, input string) (fantasy.ToolResponse, error) {
-	var args map[string]any
-	if err := json.Unmarshal([]byte(input), &args); err != nil {
-		return fantasy.NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
-	}
-
-	c, err := getOrRenewClient(ctx, name)
-	if err != nil {
-		return fantasy.NewTextErrorResponse(err.Error()), nil
-	}
-	result, err := c.CallTool(ctx, &mcp.CallToolParams{
-		Name:      toolName,
-		Arguments: args,
-	})
-	if err != nil {
-		return fantasy.NewTextErrorResponse(err.Error()), nil
-	}
-
-	output := make([]string, 0, len(result.Content))
-	for _, v := range result.Content {
-		if vv, ok := v.(*mcp.TextContent); ok {
-			output = append(output, vv.Text)
-		} else {
-			output = append(output, fmt.Sprintf("%v", v))
-		}
-	}
-	return fantasy.NewTextResponse(strings.Join(output, "\n")), nil
-}
-
-func getOrRenewClient(ctx context.Context, name string) (*mcp.ClientSession, error) {
-	sess, ok := mcpClients.Get(name)
-	if !ok {
-		return nil, fmt.Errorf("mcp '%s' not available", name)
-	}
-
-	cfg := config.Get()
-	m := cfg.MCP[name]
-	state, _ := mcpStates.Get(name)
-
-	timeout := mcpTimeout(m)
-	pingCtx, cancel := context.WithTimeout(ctx, timeout)
-	defer cancel()
-	err := sess.Ping(pingCtx, nil)
-	if err == nil {
-		return sess, nil
-	}
-	updateMCPState(name, MCPStateError, maybeTimeoutErr(err, timeout), nil, state.ToolCount)
-
-	sess, err = createMCPSession(ctx, name, m, cfg.Resolver())
-	if err != nil {
-		return nil, err
-	}
-
-	updateMCPState(name, MCPStateConnected, nil, sess, state.ToolCount)
-	mcpClients.Set(name, sess)
-	return sess, nil
-}
-
-func (m *McpTool) Run(ctx context.Context, params fantasy.ToolCall) (fantasy.ToolResponse, error) {
+func (m *Tool) Run(ctx context.Context, params fantasy.ToolCall) (fantasy.ToolResponse, error) {
 	sessionID := GetSessionFromContext(ctx)
 	if sessionID == "" {
 		return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
@@ -224,302 +104,9 @@ func (m *McpTool) Run(ctx context.Context, params fantasy.ToolCall) (fantasy.Too
 		return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
 	}
 
-	return runTool(ctx, m.mcpName, m.tool.Name, params.Input)
-}
-
-func getTools(ctx context.Context, name string, permissions permission.Service, c *mcp.ClientSession, workingDir string) ([]*McpTool, error) {
-	result, err := c.ListTools(ctx, &mcp.ListToolsParams{})
+	content, err := mcp.RunTool(ctx, m.mcpName, m.tool.Name, params.Input)
 	if err != nil {
-		return nil, err
-	}
-	mcpTools := make([]*McpTool, 0, len(result.Tools))
-	for _, tool := range result.Tools {
-		mcpTools = append(mcpTools, &McpTool{
-			mcpName:     name,
-			tool:        tool,
-			permissions: permissions,
-			workingDir:  workingDir,
-		})
-	}
-	return mcpTools, nil
-}
-
-// SubscribeMCPEvents returns a channel for MCP events
-func SubscribeMCPEvents(ctx context.Context) <-chan pubsub.Event[MCPEvent] {
-	return mcpBroker.Subscribe(ctx)
-}
-
-// GetMCPStates returns the current state of all MCP clients
-func GetMCPStates() map[string]MCPClientInfo {
-	return maps.Collect(mcpStates.Seq2())
-}
-
-// GetMCPState returns the state of a specific MCP client
-func GetMCPState(name string) (MCPClientInfo, bool) {
-	return mcpStates.Get(name)
-}
-
-// updateMCPState updates the state of an MCP client and publishes an event
-func updateMCPState(name string, state MCPState, err error, client *mcp.ClientSession, toolCount int) {
-	info := MCPClientInfo{
-		Name:      name,
-		State:     state,
-		Error:     err,
-		Client:    client,
-		ToolCount: toolCount,
-	}
-	switch state {
-	case MCPStateConnected:
-		info.ConnectedAt = time.Now()
-	case MCPStateError:
-		updateMcpTools(name, nil)
-		mcpClients.Del(name)
-	}
-	mcpStates.Set(name, info)
-
-	// Publish state change event
-	mcpBroker.Publish(pubsub.UpdatedEvent, MCPEvent{
-		Type:      MCPEventStateChanged,
-		Name:      name,
-		State:     state,
-		Error:     err,
-		ToolCount: toolCount,
-	})
-}
-
-// CloseMCPClients closes all MCP clients. This should be called during application shutdown.
-func CloseMCPClients() error {
-	var errs []error
-	for name, c := range mcpClients.Seq2() {
-		if err := c.Close(); err != nil &&
-			!errors.Is(err, io.EOF) &&
-			!errors.Is(err, context.Canceled) &&
-			err.Error() != "signal: killed" {
-			errs = append(errs, fmt.Errorf("close mcp: %s: %w", name, err))
-		}
-	}
-	mcpBroker.Shutdown()
-	return errors.Join(errs...)
-}
-
-func GetMCPTools(ctx context.Context, permissions permission.Service, cfg *config.Config) []*McpTool {
-	mcpToolsOnce.Do(func() {
-		var wg sync.WaitGroup
-		// Initialize states for all configured MCPs
-		for name, m := range cfg.MCP {
-			if m.Disabled {
-				updateMCPState(name, MCPStateDisabled, nil, nil, 0)
-				slog.Debug("skipping disabled mcp", "name", name)
-				continue
-			}
-
-			// Set initial starting state
-			updateMCPState(name, MCPStateStarting, nil, nil, 0)
-
-			wg.Add(1)
-			go func(name string, m config.MCPConfig) {
-				defer func() {
-					wg.Done()
-					if r := recover(); r != nil {
-						var err error
-						switch v := r.(type) {
-						case error:
-							err = v
-						case string:
-							err = fmt.Errorf("panic: %s", v)
-						default:
-							err = fmt.Errorf("panic: %v", v)
-						}
-						updateMCPState(name, MCPStateError, err, nil, 0)
-						slog.Error("panic in mcp client initialization", "error", err, "name", name)
-					}
-				}()
-
-				ctx, cancel := context.WithTimeout(ctx, mcpTimeout(m))
-				defer cancel()
-
-				c, err := createMCPSession(ctx, name, m, cfg.Resolver())
-				if err != nil {
-					return
-				}
-
-				mcpClients.Set(name, c)
-
-				tools, err := getTools(ctx, name, permissions, c, cfg.WorkingDir())
-				if err != nil {
-					slog.Error("error listing tools", "error", err)
-					updateMCPState(name, MCPStateError, err, nil, 0)
-					c.Close()
-					return
-				}
-
-				updateMcpTools(name, tools)
-				mcpClients.Set(name, c)
-				updateMCPState(name, MCPStateConnected, nil, c, len(tools))
-			}(name, m)
-		}
-		wg.Wait()
-	})
-	return slices.Collect(mcpTools.Seq())
-}
-
-// updateMcpTools updates the global mcpTools and mcpClient2Tools maps
-func updateMcpTools(mcpName string, tools []*McpTool) {
-	if len(tools) == 0 {
-		mcpClient2Tools.Del(mcpName)
-	} else {
-		mcpClient2Tools.Set(mcpName, tools)
-	}
-	for _, tools := range mcpClient2Tools.Seq2() {
-		for _, t := range tools {
-			mcpTools.Set(t.Name(), t)
-		}
-	}
-}
-
-func createMCPSession(ctx context.Context, name string, m config.MCPConfig, resolver config.VariableResolver) (*mcp.ClientSession, error) {
-	timeout := mcpTimeout(m)
-	mcpCtx, cancel := context.WithCancel(ctx)
-	cancelTimer := time.AfterFunc(timeout, cancel)
-
-	transport, err := createMCPTransport(mcpCtx, m, resolver)
-	if err != nil {
-		updateMCPState(name, MCPStateError, err, nil, 0)
-		slog.Error("error creating mcp client", "error", err, "name", name)
-		cancel()
-		cancelTimer.Stop()
-		return nil, err
-	}
-
-	client := mcp.NewClient(
-		&mcp.Implementation{
-			Name:    "crush",
-			Version: version.Version,
-			Title:   "Crush",
-		},
-		&mcp.ClientOptions{
-			ToolListChangedHandler: func(context.Context, *mcp.ToolListChangedRequest) {
-				mcpBroker.Publish(pubsub.UpdatedEvent, MCPEvent{
-					Type: MCPEventToolsListChanged,
-					Name: name,
-				})
-			},
-			KeepAlive: time.Minute * 10,
-		},
-	)
-
-	session, err := client.Connect(mcpCtx, transport, nil)
-	if err != nil {
-		err = maybeStdioErr(err, transport)
-		updateMCPState(name, MCPStateError, maybeTimeoutErr(err, timeout), nil, 0)
-		slog.Error("error starting mcp client", "error", err, "name", name)
-		cancel()
-		cancelTimer.Stop()
-		return nil, err
-	}
-
-	cancelTimer.Stop()
-	slog.Info("Initialized mcp client", "name", name)
-	return session, nil
-}
-
-// maybeStdioErr if a stdio mcp prints an error in non-json format, it'll fail
-// to parse, and the cli will then close it, causing the EOF error.
-// so, if we got an EOF err, and the transport is STDIO, we try to exec it
-// again with a timeout and collect the output so we can add details to the
-// error.
-// this happens particularly when starting things with npx, e.g. if node can't
-// be found or some other error like that.
-func maybeStdioErr(err error, transport mcp.Transport) error {
-	if !errors.Is(err, io.EOF) {
-		return err
-	}
-	ct, ok := transport.(*mcp.CommandTransport)
-	if !ok {
-		return err
-	}
-	if err2 := stdioMCPCheck(ct.Command); err2 != nil {
-		err = errors.Join(err, err2)
-	}
-	return err
-}
-
-func maybeTimeoutErr(err error, timeout time.Duration) error {
-	if errors.Is(err, context.Canceled) {
-		return fmt.Errorf("timed out after %s", timeout)
-	}
-	return err
-}
-
-func createMCPTransport(ctx context.Context, m config.MCPConfig, resolver config.VariableResolver) (mcp.Transport, error) {
-	switch m.Type {
-	case config.MCPStdio:
-		command, err := resolver.ResolveValue(m.Command)
-		if err != nil {
-			return nil, fmt.Errorf("invalid mcp command: %w", err)
-		}
-		if strings.TrimSpace(command) == "" {
-			return nil, fmt.Errorf("mcp stdio config requires a non-empty 'command' field")
-		}
-		cmd := exec.CommandContext(ctx, home.Long(command), m.Args...)
-		cmd.Env = append(os.Environ(), m.ResolvedEnv()...)
-		return &mcp.CommandTransport{
-			Command: cmd,
-		}, nil
-	case config.MCPHttp:
-		if strings.TrimSpace(m.URL) == "" {
-			return nil, fmt.Errorf("mcp http config requires a non-empty 'url' field")
-		}
-		client := &http.Client{
-			Transport: &headerRoundTripper{
-				headers: m.ResolvedHeaders(),
-			},
-		}
-		return &mcp.StreamableClientTransport{
-			Endpoint:   m.URL,
-			HTTPClient: client,
-		}, nil
-	case config.MCPSSE:
-		if strings.TrimSpace(m.URL) == "" {
-			return nil, fmt.Errorf("mcp sse config requires a non-empty 'url' field")
-		}
-		client := &http.Client{
-			Transport: &headerRoundTripper{
-				headers: m.ResolvedHeaders(),
-			},
-		}
-		return &mcp.SSEClientTransport{
-			Endpoint:   m.URL,
-			HTTPClient: client,
-		}, nil
-	default:
-		return nil, fmt.Errorf("unsupported mcp type: %s", m.Type)
-	}
-}
-
-type headerRoundTripper struct {
-	headers map[string]string
-}
-
-func (rt headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
-	for k, v := range rt.headers {
-		req.Header.Set(k, v)
-	}
-	return http.DefaultTransport.RoundTrip(req)
-}
-
-func mcpTimeout(m config.MCPConfig) time.Duration {
-	return time.Duration(cmp.Or(m.Timeout, 15)) * time.Second
-}
-
-func stdioMCPCheck(old *exec.Cmd) error {
-	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
-	defer cancel()
-	cmd := exec.CommandContext(ctx, old.Path, old.Args...)
-	cmd.Env = old.Env
-	out, err := cmd.CombinedOutput()
-	if err == nil || errors.Is(ctx.Err(), context.DeadlineExceeded) {
-		return nil
+		return fantasy.NewTextErrorResponse(err.Error()), nil
 	}
-	return fmt.Errorf("%w: %s", err, string(out))
+	return fantasy.NewTextResponse(content), nil
 }

internal/agent/tools/mcp/init.go 🔗

@@ -0,0 +1,405 @@
+// Package mcp provides functionality for managing Model Context Protocol (MCP)
+// clients within the Crush application.
+package mcp
+
+import (
+	"cmp"
+	"context"
+	"errors"
+	"fmt"
+	"io"
+	"log/slog"
+	"maps"
+	"net/http"
+	"os"
+	"os/exec"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/charmbracelet/crush/internal/config"
+	"github.com/charmbracelet/crush/internal/csync"
+	"github.com/charmbracelet/crush/internal/home"
+	"github.com/charmbracelet/crush/internal/permission"
+	"github.com/charmbracelet/crush/internal/pubsub"
+	"github.com/charmbracelet/crush/internal/version"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
+)
+
+var (
+	sessions = csync.NewMap[string, *mcp.ClientSession]()
+	states   = csync.NewMap[string, ClientInfo]()
+	broker   = pubsub.NewBroker[Event]()
+)
+
+// State represents the current state of an MCP client
+type State int
+
+const (
+	StateDisabled State = iota
+	StateStarting
+	StateConnected
+	StateError
+)
+
+func (s State) String() string {
+	switch s {
+	case StateDisabled:
+		return "disabled"
+	case StateStarting:
+		return "starting"
+	case StateConnected:
+		return "connected"
+	case StateError:
+		return "error"
+	default:
+		return "unknown"
+	}
+}
+
+// EventType represents the type of MCP event
+type EventType uint
+
+const (
+	EventStateChanged EventType = iota
+	EventToolsListChanged
+	EventPromptsListChanged
+)
+
+// Event represents an event in the MCP system
+type Event struct {
+	Type   EventType
+	Name   string
+	State  State
+	Error  error
+	Counts Counts
+}
+
+// Counts number of available tools, prompts, etc.
+type Counts struct {
+	Tools   int
+	Prompts int
+}
+
+// ClientInfo holds information about an MCP client's state
+type ClientInfo struct {
+	Name        string
+	State       State
+	Error       error
+	Client      *mcp.ClientSession
+	Counts      Counts
+	ConnectedAt time.Time
+}
+
+// SubscribeEvents returns a channel for MCP events
+func SubscribeEvents(ctx context.Context) <-chan pubsub.Event[Event] {
+	return broker.Subscribe(ctx)
+}
+
+// GetStates returns the current state of all MCP clients
+func GetStates() map[string]ClientInfo {
+	return maps.Collect(states.Seq2())
+}
+
+// GetState returns the state of a specific MCP client
+func GetState(name string) (ClientInfo, bool) {
+	return states.Get(name)
+}
+
+// Close closes all MCP clients. This should be called during application shutdown.
+func Close() error {
+	var errs []error
+	for name, c := range sessions.Seq2() {
+		if err := c.Close(); err != nil &&
+			!errors.Is(err, io.EOF) &&
+			!errors.Is(err, context.Canceled) &&
+			err.Error() != "signal: killed" {
+			errs = append(errs, fmt.Errorf("close mcp: %s: %w", name, err))
+		}
+	}
+	broker.Shutdown()
+	return errors.Join(errs...)
+}
+
+// Initialize initializes MCP clients based on the provided configuration.
+func Initialize(ctx context.Context, permissions permission.Service, cfg *config.Config) {
+	var wg sync.WaitGroup
+	// Initialize states for all configured MCPs
+	for name, m := range cfg.MCP {
+		if m.Disabled {
+			updateState(name, StateDisabled, nil, nil, Counts{})
+			slog.Debug("skipping disabled mcp", "name", name)
+			continue
+		}
+
+		// Set initial starting state
+		updateState(name, StateStarting, nil, nil, Counts{})
+
+		wg.Add(1)
+		go func(name string, m config.MCPConfig) {
+			defer func() {
+				wg.Done()
+				if r := recover(); r != nil {
+					var err error
+					switch v := r.(type) {
+					case error:
+						err = v
+					case string:
+						err = fmt.Errorf("panic: %s", v)
+					default:
+						err = fmt.Errorf("panic: %v", v)
+					}
+					updateState(name, StateError, err, nil, Counts{})
+					slog.Error("panic in mcp client initialization", "error", err, "name", name)
+				}
+			}()
+
+			ctx, cancel := context.WithTimeout(ctx, mcpTimeout(m))
+			defer cancel()
+
+			session, err := createSession(ctx, name, m, cfg.Resolver())
+			if err != nil {
+				return
+			}
+
+			tools, err := getTools(ctx, session)
+			if err != nil {
+				slog.Error("error listing tools", "error", err)
+				updateState(name, StateError, err, nil, Counts{})
+				session.Close()
+				return
+			}
+
+			prompts, err := getPrompts(ctx, session)
+			if err != nil {
+				slog.Error("error listing prompts", "error", err)
+				updateState(name, StateError, err, nil, Counts{})
+				session.Close()
+				return
+			}
+
+			updateTools(name, tools)
+			updatePrompts(name, prompts)
+			sessions.Set(name, session)
+
+			updateState(name, StateConnected, nil, session, Counts{
+				Tools:   len(tools),
+				Prompts: len(prompts),
+			})
+		}(name, m)
+	}
+	wg.Wait()
+}
+
+func getOrRenewClient(ctx context.Context, name string) (*mcp.ClientSession, error) {
+	sess, ok := sessions.Get(name)
+	if !ok {
+		return nil, fmt.Errorf("mcp '%s' not available", name)
+	}
+
+	cfg := config.Get()
+	m := cfg.MCP[name]
+	state, _ := states.Get(name)
+
+	timeout := mcpTimeout(m)
+	pingCtx, cancel := context.WithTimeout(ctx, timeout)
+	defer cancel()
+	err := sess.Ping(pingCtx, nil)
+	if err == nil {
+		return sess, nil
+	}
+	updateState(name, StateError, maybeTimeoutErr(err, timeout), nil, state.Counts)
+
+	sess, err = createSession(ctx, name, m, cfg.Resolver())
+	if err != nil {
+		return nil, err
+	}
+
+	updateState(name, StateConnected, nil, sess, state.Counts)
+	sessions.Set(name, sess)
+	return sess, nil
+}
+
+// updateState updates the state of an MCP client and publishes an event
+func updateState(name string, state State, err error, client *mcp.ClientSession, counts Counts) {
+	info := ClientInfo{
+		Name:   name,
+		State:  state,
+		Error:  err,
+		Client: client,
+		Counts: counts,
+	}
+	switch state {
+	case StateConnected:
+		info.ConnectedAt = time.Now()
+	case StateError:
+		updateTools(name, nil)
+		sessions.Del(name)
+	}
+	states.Set(name, info)
+
+	// Publish state change event
+	broker.Publish(pubsub.UpdatedEvent, Event{
+		Type:   EventStateChanged,
+		Name:   name,
+		State:  state,
+		Error:  err,
+		Counts: counts,
+	})
+}
+
+func createSession(ctx context.Context, name string, m config.MCPConfig, resolver config.VariableResolver) (*mcp.ClientSession, error) {
+	timeout := mcpTimeout(m)
+	mcpCtx, cancel := context.WithCancel(ctx)
+	cancelTimer := time.AfterFunc(timeout, cancel)
+
+	transport, err := createTransport(mcpCtx, m, resolver)
+	if err != nil {
+		updateState(name, StateError, err, nil, Counts{})
+		slog.Error("error creating mcp client", "error", err, "name", name)
+		cancel()
+		cancelTimer.Stop()
+		return nil, err
+	}
+
+	client := mcp.NewClient(
+		&mcp.Implementation{
+			Name:    "crush",
+			Version: version.Version,
+			Title:   "Crush",
+		},
+		&mcp.ClientOptions{
+			ToolListChangedHandler: func(context.Context, *mcp.ToolListChangedRequest) {
+				broker.Publish(pubsub.UpdatedEvent, Event{
+					Type: EventToolsListChanged,
+					Name: name,
+				})
+			},
+			PromptListChangedHandler: func(context.Context, *mcp.PromptListChangedRequest) {
+				broker.Publish(pubsub.UpdatedEvent, Event{
+					Type: EventPromptsListChanged,
+					Name: name,
+				})
+			},
+			LoggingMessageHandler: func(_ context.Context, req *mcp.LoggingMessageRequest) {
+				slog.Info("mcp log", "name", name, "data", req.Params.Data)
+			},
+			KeepAlive: time.Minute * 10,
+		},
+	)
+
+	session, err := client.Connect(mcpCtx, transport, nil)
+	if err != nil {
+		err = maybeStdioErr(err, transport)
+		updateState(name, StateError, maybeTimeoutErr(err, timeout), nil, Counts{})
+		slog.Error("error starting mcp client", "error", err, "name", name)
+		cancel()
+		cancelTimer.Stop()
+		return nil, err
+	}
+
+	cancelTimer.Stop()
+	slog.Info("Initialized mcp client", "name", name)
+	return session, nil
+}
+
+// maybeStdioErr if a stdio mcp prints an error in non-json format, it'll fail
+// to parse, and the cli will then close it, causing the EOF error.
+// so, if we got an EOF err, and the transport is STDIO, we try to exec it
+// again with a timeout and collect the output so we can add details to the
+// error.
+// this happens particularly when starting things with npx, e.g. if node can't
+// be found or some other error like that.
+func maybeStdioErr(err error, transport mcp.Transport) error {
+	if !errors.Is(err, io.EOF) {
+		return err
+	}
+	ct, ok := transport.(*mcp.CommandTransport)
+	if !ok {
+		return err
+	}
+	if err2 := stdioCheck(ct.Command); err2 != nil {
+		err = errors.Join(err, err2)
+	}
+	return err
+}
+
+func maybeTimeoutErr(err error, timeout time.Duration) error {
+	if errors.Is(err, context.Canceled) {
+		return fmt.Errorf("timed out after %s", timeout)
+	}
+	return err
+}
+
+func createTransport(ctx context.Context, m config.MCPConfig, resolver config.VariableResolver) (mcp.Transport, error) {
+	switch m.Type {
+	case config.MCPStdio:
+		command, err := resolver.ResolveValue(m.Command)
+		if err != nil {
+			return nil, fmt.Errorf("invalid mcp command: %w", err)
+		}
+		if strings.TrimSpace(command) == "" {
+			return nil, fmt.Errorf("mcp stdio config requires a non-empty 'command' field")
+		}
+		cmd := exec.CommandContext(ctx, home.Long(command), m.Args...)
+		cmd.Env = append(os.Environ(), m.ResolvedEnv()...)
+		return &mcp.CommandTransport{
+			Command: cmd,
+		}, nil
+	case config.MCPHttp:
+		if strings.TrimSpace(m.URL) == "" {
+			return nil, fmt.Errorf("mcp http config requires a non-empty 'url' field")
+		}
+		client := &http.Client{
+			Transport: &headerRoundTripper{
+				headers: m.ResolvedHeaders(),
+			},
+		}
+		return &mcp.StreamableClientTransport{
+			Endpoint:   m.URL,
+			HTTPClient: client,
+		}, nil
+	case config.MCPSSE:
+		if strings.TrimSpace(m.URL) == "" {
+			return nil, fmt.Errorf("mcp sse config requires a non-empty 'url' field")
+		}
+		client := &http.Client{
+			Transport: &headerRoundTripper{
+				headers: m.ResolvedHeaders(),
+			},
+		}
+		return &mcp.SSEClientTransport{
+			Endpoint:   m.URL,
+			HTTPClient: client,
+		}, nil
+	default:
+		return nil, fmt.Errorf("unsupported mcp type: %s", m.Type)
+	}
+}
+
+type headerRoundTripper struct {
+	headers map[string]string
+}
+
+func (rt headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	for k, v := range rt.headers {
+		req.Header.Set(k, v)
+	}
+	return http.DefaultTransport.RoundTrip(req)
+}
+
+func mcpTimeout(m config.MCPConfig) time.Duration {
+	return time.Duration(cmp.Or(m.Timeout, 15)) * time.Second
+}
+
+func stdioCheck(old *exec.Cmd) error {
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+	defer cancel()
+	cmd := exec.CommandContext(ctx, old.Path, old.Args...)
+	cmd.Env = old.Env
+	out, err := cmd.CombinedOutput()
+	if err == nil || errors.Is(ctx.Err(), context.DeadlineExceeded) {
+		return nil
+	}
+	return fmt.Errorf("%w: %s", err, string(out))
+}

internal/agent/tools/mcp/prompts.go 🔗

@@ -0,0 +1,87 @@
+package mcp
+
+import (
+	"context"
+	"iter"
+	"log/slog"
+
+	"github.com/charmbracelet/crush/internal/csync"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
+)
+
+type Prompt = mcp.Prompt
+
+var allPrompts = csync.NewMap[string, []*Prompt]()
+
+// Prompts returns all available MCP prompts.
+func Prompts() iter.Seq2[string, []*Prompt] {
+	return allPrompts.Seq2()
+}
+
+// GetPromptMessages retrieves the content of an MCP prompt with the given arguments.
+func GetPromptMessages(ctx context.Context, clientName, promptName string, args map[string]string) ([]string, error) {
+	c, err := getOrRenewClient(ctx, clientName)
+	if err != nil {
+		return nil, err
+	}
+	result, err := c.GetPrompt(ctx, &mcp.GetPromptParams{
+		Name:      promptName,
+		Arguments: args,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	var messages []string
+	for _, msg := range result.Messages {
+		if msg.Role != "user" {
+			continue
+		}
+		if textContent, ok := msg.Content.(*mcp.TextContent); ok {
+			messages = append(messages, textContent.Text)
+		}
+	}
+	return messages, nil
+}
+
+// RefreshPrompts gets the updated list of prompts from the MCP and updates the
+// global state.
+func RefreshPrompts(ctx context.Context, name string) {
+	session, ok := sessions.Get(name)
+	if !ok {
+		slog.Warn("refresh prompts: no session", "name", name)
+		return
+	}
+
+	prompts, err := getPrompts(ctx, session)
+	if err != nil {
+		updateState(name, StateError, err, nil, Counts{})
+		return
+	}
+
+	updatePrompts(name, prompts)
+
+	prev, _ := states.Get(name)
+	prev.Counts.Prompts = len(prompts)
+	updateState(name, StateConnected, nil, session, prev.Counts)
+}
+
+func getPrompts(ctx context.Context, c *mcp.ClientSession) ([]*Prompt, error) {
+	if c.InitializeResult().Capabilities.Prompts == nil {
+		return nil, nil
+	}
+	result, err := c.ListPrompts(ctx, &mcp.ListPromptsParams{})
+	if err != nil {
+		return nil, err
+	}
+	return result.Prompts, nil
+}
+
+// updatePrompts updates the global mcpPrompts and mcpClient2Prompts maps
+func updatePrompts(mcpName string, prompts []*Prompt) {
+	if len(prompts) == 0 {
+		allPrompts.Del(mcpName)
+		return
+	}
+	allPrompts.Set(mcpName, prompts)
+}

internal/agent/tools/mcp/tools.go 🔗

@@ -0,0 +1,93 @@
+package mcp
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"iter"
+	"log/slog"
+	"strings"
+
+	"github.com/charmbracelet/crush/internal/csync"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
+)
+
+type Tool = mcp.Tool
+
+var allTools = csync.NewMap[string, []*Tool]()
+
+// Tools returns all available MCP tools.
+func Tools() iter.Seq2[string, []*Tool] {
+	return allTools.Seq2()
+}
+
+// RunTool runs an MCP tool with the given input parameters.
+func RunTool(ctx context.Context, name, toolName string, input string) (string, error) {
+	var args map[string]any
+	if err := json.Unmarshal([]byte(input), &args); err != nil {
+		return "", fmt.Errorf("error parsing parameters: %s", err)
+	}
+
+	c, err := getOrRenewClient(ctx, name)
+	if err != nil {
+		return "", err
+	}
+	result, err := c.CallTool(ctx, &mcp.CallToolParams{
+		Name:      toolName,
+		Arguments: args,
+	})
+	if err != nil {
+		return "", err
+	}
+
+	output := make([]string, 0, len(result.Content))
+	for _, v := range result.Content {
+		if vv, ok := v.(*mcp.TextContent); ok {
+			output = append(output, vv.Text)
+		} else {
+			output = append(output, fmt.Sprintf("%v", v))
+		}
+	}
+	return strings.Join(output, "\n"), nil
+}
+
+// RefreshTools gets the updated list of tools from the MCP and updates the
+// global state.
+func RefreshTools(ctx context.Context, name string) {
+	session, ok := sessions.Get(name)
+	if !ok {
+		slog.Warn("refresh tools: no session", "name", name)
+		return
+	}
+
+	tools, err := getTools(ctx, session)
+	if err != nil {
+		updateState(name, StateError, err, nil, Counts{})
+		return
+	}
+
+	updateTools(name, tools)
+
+	prev, _ := states.Get(name)
+	prev.Counts.Tools = len(tools)
+	updateState(name, StateConnected, nil, session, prev.Counts)
+}
+
+func getTools(ctx context.Context, session *mcp.ClientSession) ([]*Tool, error) {
+	if session.InitializeResult().Capabilities.Tools == nil {
+		return nil, nil
+	}
+	result, err := session.ListTools(ctx, &mcp.ListToolsParams{})
+	if err != nil {
+		return nil, err
+	}
+	return result.Tools, nil
+}
+
+func updateTools(name string, tools []*Tool) {
+	if len(tools) == 0 {
+		allTools.Del(name)
+		return
+	}
+	allTools.Set(name, tools)
+}

internal/agent/tools/view.go 🔗

@@ -288,8 +288,13 @@ type LineScanner struct {
 }
 
 func NewLineScanner(r io.Reader) *LineScanner {
+	scanner := bufio.NewScanner(r)
+	// Increase buffer size to handle large lines (e.g., minified JSON, HTML)
+	// Default is 64KB, set to 1MB
+	buf := make([]byte, 0, 64*1024)
+	scanner.Buffer(buf, 1024*1024)
 	return &LineScanner{
-		scanner: bufio.NewScanner(r),
+		scanner: scanner,
 	}
 }
 

internal/agent/tools/web_fetch.go 🔗

@@ -0,0 +1,72 @@
+package tools
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"net/http"
+	"os"
+	"strings"
+	"time"
+
+	"charm.land/fantasy"
+)
+
+//go:embed web_fetch.md
+var webFetchToolDescription []byte
+
+// NewWebFetchTool creates a simple web fetch tool for sub-agents (no permissions needed).
+func NewWebFetchTool(workingDir string, client *http.Client) fantasy.AgentTool {
+	if client == nil {
+		client = &http.Client{
+			Timeout: 30 * time.Second,
+			Transport: &http.Transport{
+				MaxIdleConns:        100,
+				MaxIdleConnsPerHost: 10,
+				IdleConnTimeout:     90 * time.Second,
+			},
+		}
+	}
+
+	return fantasy.NewAgentTool(
+		WebFetchToolName,
+		string(webFetchToolDescription),
+		func(ctx context.Context, params WebFetchParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
+			if params.URL == "" {
+				return fantasy.NewTextErrorResponse("url is required"), nil
+			}
+
+			content, err := FetchURLAndConvert(ctx, client, params.URL)
+			if err != nil {
+				return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to fetch URL: %s", err)), nil
+			}
+
+			hasLargeContent := len(content) > LargeContentThreshold
+			var result strings.Builder
+
+			if hasLargeContent {
+				tempFile, err := os.CreateTemp(workingDir, "page-*.md")
+				if err != nil {
+					return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to create temporary file: %s", err)), nil
+				}
+				tempFilePath := tempFile.Name()
+
+				if _, err := tempFile.WriteString(content); err != nil {
+					_ = tempFile.Close() // Best effort close
+					return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to write content to file: %s", err)), nil
+				}
+				if err := tempFile.Close(); err != nil {
+					return fantasy.NewTextErrorResponse(fmt.Sprintf("Failed to close temporary file: %s", err)), nil
+				}
+
+				result.WriteString(fmt.Sprintf("Fetched content from %s (large page)\n\n", params.URL))
+				result.WriteString(fmt.Sprintf("Content saved to: %s\n\n", tempFilePath))
+				result.WriteString("Use the view and grep tools to analyze this file.")
+			} else {
+				result.WriteString(fmt.Sprintf("Fetched content from %s:\n\n", params.URL))
+				result.WriteString(content)
+			}
+
+			return fantasy.NewTextResponse(result.String()), nil
+		})
+}

internal/agent/tools/web_fetch.md 🔗

@@ -0,0 +1,28 @@
+Fetches content from a web URL (for use by sub-agents).
+
+<usage>
+- Provide a URL to fetch
+- The tool fetches the content and returns it as markdown
+- Use this when you need to follow links from the current page
+- After fetching, analyze the content to answer the user's question
+</usage>
+
+<features>
+- Automatically converts HTML to markdown for easier analysis
+- For large pages (>50KB), saves content to a temporary file and provides the path
+- You can then use grep/view tools to search through the file
+- Handles UTF-8 content validation
+</features>
+
+<limitations>
+- Max response size: 5MB
+- Only supports HTTP and HTTPS protocols
+- Cannot handle authentication or cookies
+- Some websites may block automated requests
+</limitations>
+
+<tips>
+- For large pages saved to files, use grep to find relevant sections first
+- Don't fetch unnecessary pages - only when needed to answer the question
+- Focus on extracting specific information from the fetched content
+</tips>

internal/app/app.go 🔗

@@ -1,3 +1,5 @@
+// Package app wires together services, coordinates agents, and manages
+// application lifecycle.
 package app
 
 import (
@@ -5,14 +7,17 @@ import (
 	"database/sql"
 	"errors"
 	"fmt"
+	"io"
 	"log/slog"
+	"os"
 	"sync"
 	"time"
 
+	tea "charm.land/bubbletea/v2"
 	"charm.land/fantasy"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/agent"
-	"github.com/charmbracelet/crush/internal/agent/tools"
+	"github.com/charmbracelet/crush/internal/agent/tools/mcp"
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/csync"
 	"github.com/charmbracelet/crush/internal/db"
@@ -24,7 +29,11 @@ import (
 	"github.com/charmbracelet/crush/internal/permission"
 	"github.com/charmbracelet/crush/internal/pubsub"
 	"github.com/charmbracelet/crush/internal/session"
+	"github.com/charmbracelet/crush/internal/term"
+	"github.com/charmbracelet/crush/internal/tui/components/anim"
+	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/x/ansi"
+	"github.com/charmbracelet/x/exp/charmtone"
 )
 
 type App struct {
@@ -82,8 +91,13 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
 	// Initialize LSP clients in the background.
 	app.initLSPClients(ctx)
 
+	go func() {
+		slog.Info("Initializing MCP clients")
+		mcp.Initialize(ctx, app.Permissions, cfg)
+	}()
+
 	// cleanup database upon app shutdown
-	app.cleanupFuncs = append(app.cleanupFuncs, conn.Close)
+	app.cleanupFuncs = append(app.cleanupFuncs, conn.Close, mcp.Close)
 
 	// TODO: remove the concept of agent config, most likely.
 	if !cfg.IsConfigured() {
@@ -101,9 +115,9 @@ func (app *App) Config() *config.Config {
 	return app.config
 }
 
-// RunNonInteractive handles the execution flow when a prompt is provided via
-// CLI flag.
-func (app *App) RunNonInteractive(ctx context.Context, prompt string, quiet bool) error {
+// RunNonInteractive runs the application in non-interactive mode with the
+// given prompt, printing to stdout.
+func (app *App) RunNonInteractive(ctx context.Context, output io.Writer, prompt string, quiet bool) error {
 	slog.Info("Running in non-interactive mode")
 
 	ctx, cancel := context.WithCancel(ctx)
@@ -111,7 +125,25 @@ func (app *App) RunNonInteractive(ctx context.Context, prompt string, quiet bool
 
 	var spinner *format.Spinner
 	if !quiet {
-		spinner = format.NewSpinner(ctx, cancel, "Generating")
+		t := styles.CurrentTheme()
+
+		// Detect background color to set the appropriate color for the
+		// spinner's 'Generating...' text. Without this, that text would be
+		// unreadable in light terminals.
+		hasDarkBG := true
+		if f, ok := output.(*os.File); ok {
+			hasDarkBG = lipgloss.HasDarkBackground(os.Stdin, f)
+		}
+		defaultFG := lipgloss.LightDark(hasDarkBG)(charmtone.Pepper, t.FgBase)
+
+		spinner = format.NewSpinner(ctx, cancel, anim.Settings{
+			Size:        10,
+			Label:       "Generating",
+			LabelColor:  defaultFG,
+			GradColorA:  t.Primary,
+			GradColorB:  t.Secondary,
+			CycleColors: true,
+		})
 		spinner.Start()
 	}
 
@@ -125,7 +157,7 @@ func (app *App) RunNonInteractive(ctx context.Context, prompt string, quiet bool
 	defer stopSpinner()
 
 	const maxPromptLengthForTitle = 100
-	titlePrefix := "Non-interactive: "
+	const titlePrefix = "Non-interactive: "
 	var titleSuffix string
 
 	if len(prompt) > maxPromptLengthForTitle {
@@ -141,7 +173,8 @@ func (app *App) RunNonInteractive(ctx context.Context, prompt string, quiet bool
 	}
 	slog.Info("Created session for non-interactive run", "session_id", sess.ID)
 
-	// Automatically approve all permission requests for this non-interactive session
+	// Automatically approve all permission requests for this non-interactive
+	// session.
 	app.Permissions.AutoApproveSession(sess.ID)
 
 	type response struct {
@@ -164,12 +197,25 @@ func (app *App) RunNonInteractive(ctx context.Context, prompt string, quiet bool
 
 	messageEvents := app.Messages.Subscribe(ctx)
 	messageReadBytes := make(map[string]int)
+	supportsProgressBar := term.SupportsProgressBar()
+
+	defer func() {
+		if supportsProgressBar {
+			_, _ = fmt.Fprintf(os.Stderr, ansi.ResetProgressBar)
+		}
+
+		// Always print a newline at the end. If output is a TTY this will
+		// prevent the prompt from overwriting the last line of output.
+		_, _ = fmt.Fprintln(output)
+	}()
 
-	defer fmt.Printf(ansi.ResetProgressBar)
 	for {
-		// HACK: add it again on every iteration so it doesn't get hidden by
-		// the terminal due to inactivity.
-		fmt.Printf(ansi.SetIndeterminateProgressBar)
+		if supportsProgressBar {
+			// HACK: Reinitialize the terminal progress bar on every iteration so
+			// it doesn't get hidden by the terminal due to inactivity.
+			_, _ = fmt.Fprintf(os.Stderr, ansi.SetIndeterminateProgressBar)
+		}
+
 		select {
 		case result := <-done:
 			stopSpinner()
@@ -196,7 +242,7 @@ func (app *App) RunNonInteractive(ctx context.Context, prompt string, quiet bool
 				}
 
 				part := content[readBytes:]
-				fmt.Print(part)
+				fmt.Fprint(output, part)
 				messageReadBytes[msg.ID] = len(content)
 			}
 
@@ -219,7 +265,7 @@ func (app *App) setupEvents() {
 	setupSubscriber(ctx, app.serviceEventsWG, "permissions", app.Permissions.Subscribe, app.events)
 	setupSubscriber(ctx, app.serviceEventsWG, "permissions-notifications", app.Permissions.SubscribeNotifications, app.events)
 	setupSubscriber(ctx, app.serviceEventsWG, "history", app.History.Subscribe, app.events)
-	setupSubscriber(ctx, app.serviceEventsWG, "mcp", tools.SubscribeMCPEvents, app.events)
+	setupSubscriber(ctx, app.serviceEventsWG, "mcp", mcp.SubscribeEvents, app.events)
 	setupSubscriber(ctx, app.serviceEventsWG, "lsp", SubscribeLSPEvents, app.events)
 	cleanupFunc := func() error {
 		cancel()
@@ -281,9 +327,6 @@ func (app *App) InitCoderAgent(ctx context.Context) error {
 		slog.Error("Failed to create coder agent", "err", err)
 		return err
 	}
-
-	// Add MCP client cleanup to shutdown process
-	app.cleanupFuncs = append(app.cleanupFuncs, tools.CloseMCPClients)
 	return nil
 }
 

internal/cmd/dirs.go 🔗

@@ -4,9 +4,9 @@ import (
 	"os"
 	"path/filepath"
 
+	"charm.land/lipgloss/v2"
+	"charm.land/lipgloss/v2/table"
 	"github.com/charmbracelet/crush/internal/config"
-	"github.com/charmbracelet/lipgloss/v2"
-	"github.com/charmbracelet/lipgloss/v2/table"
 	"github.com/charmbracelet/x/term"
 	"github.com/spf13/cobra"
 )

internal/cmd/root.go 🔗

@@ -12,19 +12,21 @@ import (
 	"strconv"
 	"strings"
 
-	tea "github.com/charmbracelet/bubbletea/v2"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/colorprofile"
 	"github.com/charmbracelet/crush/internal/app"
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/db"
 	"github.com/charmbracelet/crush/internal/event"
+	termutil "github.com/charmbracelet/crush/internal/term"
 	"github.com/charmbracelet/crush/internal/tui"
 	"github.com/charmbracelet/crush/internal/ui/common"
 	ui "github.com/charmbracelet/crush/internal/ui/model"
 	"github.com/charmbracelet/crush/internal/version"
 	"github.com/charmbracelet/fang"
-	"github.com/charmbracelet/lipgloss/v2"
 	uv "github.com/charmbracelet/ultraviolet"
+	"github.com/charmbracelet/x/ansi"
 	"github.com/charmbracelet/x/exp/charmtone"
 	"github.com/charmbracelet/x/term"
 	"github.com/spf13/cobra"
@@ -34,7 +36,6 @@ func init() {
 	rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
 	rootCmd.PersistentFlags().StringP("data-dir", "D", "", "Custom crush data directory")
 	rootCmd.PersistentFlags().BoolP("debug", "d", false, "Debug")
-
 	rootCmd.Flags().BoolP("help", "h", false, "Help")
 	rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
 
@@ -76,7 +77,7 @@ crush run "Explain the use of context in Go"
 crush -y
   `,
 	RunE: func(cmd *cobra.Command, args []string) error {
-		app, err := setupApp(cmd)
+		app, err := setupAppWithProgressBar(cmd)
 		if err != nil {
 			return err
 		}
@@ -94,7 +95,6 @@ crush -y
 			tea.WithEnvironment(env),
 			tea.WithContext(cmd.Context()),
 			tea.WithFilter(tui.MouseEventFilter)) // Filter mouse events based on focus state
-
 		go app.Subscribe(program)
 
 		if _, err := program.Run(); err != nil {
@@ -152,6 +152,15 @@ func Execute() {
 	}
 }
 
+func setupAppWithProgressBar(cmd *cobra.Command) (*app.App, error) {
+	if termutil.SupportsProgressBar() {
+		_, _ = fmt.Fprintf(os.Stderr, ansi.SetIndeterminateProgressBar)
+		defer func() { _, _ = fmt.Fprintf(os.Stderr, ansi.ResetProgressBar) }()
+	}
+
+	return setupApp(cmd)
+}
+
 // setupApp handles the common setup logic for both interactive and non-interactive modes.
 // It returns the app instance, config, cleanup function, and any error.
 func setupApp(cmd *cobra.Command) (*app.App, error) {

internal/cmd/run.go 🔗

@@ -3,6 +3,7 @@ package cmd
 import (
 	"fmt"
 	"log/slog"
+	"os"
 	"strings"
 
 	"github.com/spf13/cobra"
@@ -18,10 +19,13 @@ The prompt can be provided as arguments or piped from stdin.`,
 crush run Explain the use of context in Go
 
 # Pipe input from stdin
-echo "What is this code doing?" | crush run
+curl https://charm.land | crush run "Summarize this website"
 
-# Run with quiet mode (no spinner)
-crush run -q "Generate a README for this project"
+# Read from a file
+crush run "What is this code doing?" <<< prrr.go
+
+# Run in quiet mode (hide the spinner)
+crush run --quiet "Generate a README for this project"
   `,
 	RunE: func(cmd *cobra.Command, args []string) error {
 		quiet, _ := cmd.Flags().GetBool("quiet")
@@ -48,8 +52,13 @@ crush run -q "Generate a README for this project"
 			return fmt.Errorf("no prompt provided")
 		}
 
-		// Run non-interactive flow using the App method
-		return app.RunNonInteractive(cmd.Context(), prompt, quiet)
+		// TODO: Make this work when redirected to something other than stdout.
+		// For example:
+		//     crush run "Do something fancy" > output.txt
+		//     echo "Do something fancy" | crush run > output.txt
+		//
+		// TODO: We currently need to press ^c twice to cancel. Fix that.
+		return app.RunNonInteractive(cmd.Context(), os.Stdout, prompt, quiet)
 	},
 }
 

internal/cmd/update_providers.go 🔗

@@ -4,8 +4,8 @@ import (
 	"fmt"
 	"log/slog"
 
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/config"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/x/exp/charmtone"
 	"github.com/spf13/cobra"
 )

internal/config/config.go 🔗

@@ -475,6 +475,7 @@ func allToolNames() []string {
 		"lsp_diagnostics",
 		"lsp_references",
 		"fetch",
+		"agentic_fetch",
 		"glob",
 		"grep",
 		"ls",

internal/config/load.go 🔗

@@ -662,17 +662,6 @@ func GlobalConfig() string {
 		return filepath.Join(xdgConfigHome, appName, fmt.Sprintf("%s.json", appName))
 	}
 
-	// return the path to the main config directory
-	// for windows, it should be in `%LOCALAPPDATA%/crush/`
-	// for linux and macOS, it should be in `$HOME/.config/crush/`
-	if runtime.GOOS == "windows" {
-		localAppData := os.Getenv("LOCALAPPDATA")
-		if localAppData == "" {
-			localAppData = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local")
-		}
-		return filepath.Join(localAppData, appName, fmt.Sprintf("%s.json", appName))
-	}
-
 	return filepath.Join(home.Dir(), ".config", appName, fmt.Sprintf("%s.json", appName))
 }
 

internal/config/load_test.go 🔗

@@ -485,7 +485,7 @@ func TestConfig_setupAgentsWithDisabledTools(t *testing.T) {
 	cfg.SetupAgents()
 	coderAgent, ok := cfg.Agents[AgentCoder]
 	require.True(t, ok)
-	assert.Equal(t, []string{"agent", "bash", "multiedit", "lsp_diagnostics", "lsp_references", "fetch", "glob", "ls", "sourcegraph", "view", "write"}, coderAgent.AllowedTools)
+	assert.Equal(t, []string{"agent", "bash", "multiedit", "lsp_diagnostics", "lsp_references", "fetch", "agentic_fetch", "glob", "ls", "sourcegraph", "view", "write"}, coderAgent.AllowedTools)
 
 	taskAgent, ok := cfg.Agents[AgentTask]
 	require.True(t, ok)
@@ -508,7 +508,7 @@ func TestConfig_setupAgentsWithEveryReadOnlyToolDisabled(t *testing.T) {
 	cfg.SetupAgents()
 	coderAgent, ok := cfg.Agents[AgentCoder]
 	require.True(t, ok)
-	assert.Equal(t, []string{"agent", "bash", "download", "edit", "multiedit", "lsp_diagnostics", "lsp_references", "fetch", "write"}, coderAgent.AllowedTools)
+	assert.Equal(t, []string{"agent", "bash", "download", "edit", "multiedit", "lsp_diagnostics", "lsp_references", "fetch", "agentic_fetch", "write"}, coderAgent.AllowedTools)
 
 	taskAgent, ok := cfg.Agents[AgentTask]
 	require.True(t, ok)

internal/config/provider.go 🔗

@@ -10,7 +10,6 @@ import (
 	"runtime"
 	"strings"
 	"sync"
-	"time"
 
 	"github.com/charmbracelet/catwalk/pkg/catwalk"
 	"github.com/charmbracelet/catwalk/pkg/embedded"
@@ -49,7 +48,7 @@ func providerCacheFileData() string {
 }
 
 func saveProvidersInCache(path string, providers []catwalk.Provider) error {
-	slog.Info("Saving cached provider data", "path", path)
+	slog.Info("Saving provider data to disk", "path", path)
 	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
 		return fmt.Errorf("failed to create directory for provider cache: %w", err)
 	}
@@ -126,8 +125,6 @@ func Providers(cfg *Config) ([]catwalk.Provider, error) {
 }
 
 func loadProviders(autoUpdateDisabled bool, client ProviderClient, path string) ([]catwalk.Provider, error) {
-	_, cacheExists := isCacheStale(path)
-
 	catwalkGetAndSave := func() ([]catwalk.Provider, error) {
 		providers, err := client.GetProviders()
 		if err != nil {
@@ -141,11 +138,12 @@ func loadProviders(autoUpdateDisabled bool, client ProviderClient, path string)
 		}
 		return providers, nil
 	}
+
 	switch {
 	case autoUpdateDisabled:
 		slog.Warn("Providers auto-update is disabled")
 
-		if cacheExists {
+		if _, err := os.Stat(path); err == nil {
 			slog.Warn("Using locally cached providers")
 			return loadProvidersFromCache(path)
 		}
@@ -158,7 +156,7 @@ func loadProviders(autoUpdateDisabled bool, client ProviderClient, path string)
 		return providers, nil
 
 	default:
-		slog.Info("Cache is not available or is stale. Fetching providers from Catwalk.", "path", path)
+		slog.Info("Fetching providers from Catwalk.", "path", path)
 
 		providers, err := catwalkGetAndSave()
 		if err != nil {
@@ -168,11 +166,3 @@ func loadProviders(autoUpdateDisabled bool, client ProviderClient, path string)
 		return providers, nil
 	}
 }
-
-func isCacheStale(path string) (stale, exists bool) {
-	info, err := os.Stat(path)
-	if err != nil {
-		return true, false
-	}
-	return time.Since(info.ModTime()) > 24*time.Hour, true
-}

internal/db/connect.go 🔗

@@ -23,7 +23,7 @@ func Connect(ctx context.Context, dataDir string) (*sql.DB, error) {
 	// Set pragmas for better performance
 	pragmas := []string{
 		"PRAGMA foreign_keys = ON;",
-		"PRAGMA journal_mode = WAL;",
+		"PRAGMA journal_mode = DELETE;",
 		"PRAGMA page_size = 4096;",
 		"PRAGMA cache_size = -8000;",
 		"PRAGMA synchronous = NORMAL;",

internal/format/spinner.go 🔗

@@ -6,9 +6,8 @@ import (
 	"fmt"
 	"os"
 
-	tea "github.com/charmbracelet/bubbletea/v2"
+	tea "charm.land/bubbletea/v2"
 	"github.com/charmbracelet/crush/internal/tui/components/anim"
-	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/x/ansi"
 )
 
@@ -42,28 +41,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 }
 
 // NewSpinner creates a new spinner with the given message
-func NewSpinner(ctx context.Context, cancel context.CancelFunc, message string) *Spinner {
-	t := styles.CurrentTheme()
-	model := model{
-		anim: anim.New(anim.Settings{
-			Size:        10,
-			Label:       message,
-			LabelColor:  t.FgBase,
-			GradColorA:  t.Primary,
-			GradColorB:  t.Secondary,
-			CycleColors: true,
-		}),
+func NewSpinner(ctx context.Context, cancel context.CancelFunc, animSettings anim.Settings) *Spinner {
+	m := model{
+		anim:   anim.New(animSettings),
 		cancel: cancel,
 	}
 
-	prog := tea.NewProgram(
-		model,
-		tea.WithOutput(os.Stderr),
-		tea.WithContext(ctx),
-	)
+	p := tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithContext(ctx))
 
 	return &Spinner{
-		prog: prog,
+		prog: p,
 		done: make(chan struct{}, 1),
 	}
 }

internal/home/home.go 🔗

@@ -6,38 +6,33 @@ import (
 	"os"
 	"path/filepath"
 	"strings"
-	"sync"
 )
 
-// Dir returns the users home directory, or if it fails, tries to create a new
-// temporary directory and use that instead.
-var Dir = sync.OnceValue(func() string {
-	home, err := os.UserHomeDir()
-	if err == nil {
-		slog.Debug("user home directory", "home", home)
-		return home
-	}
-	tmp, err := os.MkdirTemp("crush", "")
-	if err != nil {
-		slog.Error("could not find the user home directory")
-		return ""
+var homedir, homedirErr = os.UserHomeDir()
+
+func init() {
+	if homedirErr != nil {
+		slog.Error("failed to get user home directory", "error", homedirErr)
 	}
-	slog.Warn("could not find the user home directory, using a temporary one", "home", tmp)
-	return tmp
-})
+}
+
+// Dir returns the user home directory.
+func Dir() string {
+	return homedir
+}
 
 // Short replaces the actual home path from [Dir] with `~`.
 func Short(p string) string {
-	if !strings.HasPrefix(p, Dir()) || Dir() == "" {
+	if homedir == "" || !strings.HasPrefix(p, homedir) {
 		return p
 	}
-	return filepath.Join("~", strings.TrimPrefix(p, Dir()))
+	return filepath.Join("~", strings.TrimPrefix(p, homedir))
 }
 
 // Long replaces the `~` with actual home path from [Dir].
 func Long(p string) string {
-	if !strings.HasPrefix(p, "~") || Dir() == "" {
+	if homedir == "" || !strings.HasPrefix(p, "~") {
 		return p
 	}
-	return strings.Replace(p, "~", Dir(), 1)
+	return strings.Replace(p, "~", homedir, 1)
 }

internal/permission/permission.go 🔗

@@ -13,7 +13,7 @@ import (
 	"github.com/google/uuid"
 )
 
-var ErrorPermissionDenied = errors.New("permission denied")
+var ErrorPermissionDenied = errors.New("user denied permission")
 
 type CreatePermissionRequest struct {
 	SessionID   string `json:"session_id"`

internal/term/term.go 🔗

@@ -0,0 +1,15 @@
+package term
+
+import (
+	"os"
+	"strings"
+)
+
+// SupportsProgressBar tries to determine whether the current terminal supports
+// progress bars by looking into environment variables.
+func SupportsProgressBar() bool {
+	termProg := os.Getenv("TERM_PROGRAM")
+	_, isWindowsTerminal := os.LookupEnv("WT_SESSION")
+
+	return isWindowsTerminal || strings.Contains(strings.ToLower(termProg), "ghostty")
+}

internal/tui/components/anim/anim.go 🔗

@@ -11,8 +11,8 @@ import (
 
 	"github.com/zeebo/xxh3"
 
-	tea "github.com/charmbracelet/bubbletea/v2"
-	"github.com/charmbracelet/lipgloss/v2"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/lucasb-eyer/go-colorful"
 
 	"github.com/charmbracelet/crush/internal/csync"

internal/tui/components/chat/chat.go 🔗

@@ -5,10 +5,11 @@ import (
 	"strings"
 	"time"
 
+	"charm.land/bubbles/v2/key"
+	tea "charm.land/bubbletea/v2"
 	"github.com/atotto/clipboard"
-	"github.com/charmbracelet/bubbles/v2/key"
-	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/crush/internal/agent"
+	"github.com/charmbracelet/crush/internal/agent/tools"
 	"github.com/charmbracelet/crush/internal/app"
 	"github.com/charmbracelet/crush/internal/message"
 	"github.com/charmbracelet/crush/internal/permission"
@@ -635,8 +636,8 @@ func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResult
 	for _, tc := range msg.ToolCalls() {
 		options := m.buildToolCallOptions(tc, msg, toolResultMap)
 		uiMessages = append(uiMessages, messages.NewToolCallCmp(msg.ID, tc, m.app.Permissions, options...))
-		// If this tool call is the agent tool, fetch nested tool calls
-		if tc.Name == agent.AgentToolName {
+		// If this tool call is the agent tool or agentic fetch, fetch nested tool calls
+		if tc.Name == agent.AgentToolName || tc.Name == tools.AgenticFetchToolName {
 			agentToolSessionID := m.app.Sessions.CreateAgentToolSessionID(msg.ID, tc.ID)
 			nestedMessages, _ := m.app.Messages.List(context.Background(), agentToolSessionID)
 			nestedToolResultMap := m.buildToolResultMap(nestedMessages)

internal/tui/components/chat/editor/editor.go 🔗

@@ -13,9 +13,10 @@ import (
 	"strings"
 	"unicode"
 
-	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/bubbles/v2/textarea"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/key"
+	"charm.land/bubbles/v2/textarea"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/app"
 	"github.com/charmbracelet/crush/internal/fsext"
 	"github.com/charmbracelet/crush/internal/message"
@@ -29,7 +30,6 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/components/dialogs/quit"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 )
 
 type Editor interface {
@@ -220,7 +220,7 @@ func (m *editorCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
 		m.textarea.SetValue(msg.Text)
 		m.textarea.MoveToEnd()
 	case tea.PasteMsg:
-		path := strings.ReplaceAll(string(msg), "\\ ", " ")
+		path := strings.ReplaceAll(msg.Content, "\\ ", " ")
 		// try to get an image
 		path, err := filepath.Abs(strings.TrimSpace(path))
 		if err != nil {
@@ -264,8 +264,13 @@ func (m *editorCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
 		cur := m.textarea.Cursor()
 		curIdx := m.textarea.Width()*cur.Y + cur.X
 		switch {
+		// Open command palette when "/" is pressed on empty prompt
+		case msg.String() == "/" && len(strings.TrimSpace(m.textarea.Value())) == 0:
+			return m, util.CmdHandler(dialogs.OpenDialogMsg{
+				Model: commands.NewCommandDialog(m.session.ID),
+			})
 		// Completions
-		case msg.String() == "/" && !m.isCompletionsOpen &&
+		case msg.String() == "@" && !m.isCompletionsOpen &&
 			// only show if beginning of prompt, or if previous char is a space or newline:
 			(len(m.textarea.Value()) == 0 || unicode.IsSpace(rune(m.textarea.Value()[len(m.textarea.Value())-1]))):
 			m.isCompletionsOpen = true
@@ -337,7 +342,7 @@ func (m *editorCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
 				cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{}))
 			} else {
 				word := m.textarea.Word()
-				if strings.HasPrefix(word, "/") {
+				if strings.HasPrefix(word, "@") {
 					// XXX: wont' work if editing in the middle of the field.
 					m.completionsStartIndex = strings.LastIndex(m.textarea.Value(), word)
 					m.currentQuery = word[1:]

internal/tui/components/chat/header/header.go 🔗

@@ -4,7 +4,8 @@ import (
 	"fmt"
 	"strings"
 
-	tea "github.com/charmbracelet/bubbletea/v2"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/csync"
 	"github.com/charmbracelet/crush/internal/fsext"
@@ -13,7 +14,6 @@ import (
 	"github.com/charmbracelet/crush/internal/session"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/x/ansi"
 	"github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
 )

internal/tui/components/chat/messages/messages.go 🔗

@@ -6,11 +6,11 @@ import (
 	"strings"
 	"time"
 
-	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/bubbles/v2/viewport"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/key"
+	"charm.land/bubbles/v2/viewport"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/catwalk/pkg/catwalk"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/x/ansi"
 	"github.com/charmbracelet/x/exp/ordered"
 	"github.com/google/uuid"
@@ -262,19 +262,29 @@ func (m *messageCmp) renderThinkingContent() string {
 	if strings.TrimSpace(reasoningContent.Thinking) == "" {
 		return ""
 	}
-	lines := strings.Split(reasoningContent.Thinking, "\n")
-	var content strings.Builder
-	lineStyle := t.S().Subtle.Background(t.BgBaseLighter)
-	for i, line := range lines {
-		if line == "" {
-			continue
-		}
-		content.WriteString(lineStyle.Width(m.textWidth() - 2).Render(line))
-		if i < len(lines)-1 {
-			content.WriteString("\n")
+
+	width := m.textWidth() - 2
+	width = min(width, 120)
+
+	renderer := styles.GetPlainMarkdownRenderer(width - 1)
+	rendered, err := renderer.Render(reasoningContent.Thinking)
+	if err != nil {
+		lines := strings.Split(reasoningContent.Thinking, "\n")
+		var content strings.Builder
+		lineStyle := t.S().Subtle.Background(t.BgBaseLighter)
+		for i, line := range lines {
+			if line == "" {
+				continue
+			}
+			content.WriteString(lineStyle.Width(width).Render(line))
+			if i < len(lines)-1 {
+				content.WriteString("\n")
+			}
 		}
+		rendered = content.String()
 	}
-	fullContent := content.String()
+
+	fullContent := strings.TrimSpace(rendered)
 	height := ordered.Clamp(lipgloss.Height(fullContent), 1, 10)
 	m.thinkingViewport.SetHeight(height)
 	m.thinkingViewport.SetWidth(m.textWidth())
@@ -299,6 +309,7 @@ func (m *messageCmp) renderThinkingContent() string {
 			footer = m.anim.View()
 		}
 	}
+	lineStyle := t.S().Subtle.Background(t.BgBaseLighter)
 	return lineStyle.Width(m.textWidth()).Padding(0, 1).Render(m.thinkingViewport.View()) + "\n\n" + footer
 }
 

internal/tui/components/chat/messages/renderer.go 🔗

@@ -6,6 +6,8 @@ import (
 	"strings"
 	"time"
 
+	"charm.land/lipgloss/v2"
+	"charm.land/lipgloss/v2/tree"
 	"github.com/charmbracelet/crush/internal/agent"
 	"github.com/charmbracelet/crush/internal/agent/tools"
 	"github.com/charmbracelet/crush/internal/ansiext"
@@ -13,8 +15,6 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/components/core"
 	"github.com/charmbracelet/crush/internal/tui/highlight"
 	"github.com/charmbracelet/crush/internal/tui/styles"
-	"github.com/charmbracelet/lipgloss/v2"
-	"github.com/charmbracelet/lipgloss/v2/tree"
 	"github.com/charmbracelet/x/ansi"
 )
 
@@ -168,7 +168,9 @@ func init() {
 	registry.register(tools.EditToolName, func() renderer { return editRenderer{} })
 	registry.register(tools.MultiEditToolName, func() renderer { return multiEditRenderer{} })
 	registry.register(tools.WriteToolName, func() renderer { return writeRenderer{} })
-	registry.register(tools.FetchToolName, func() renderer { return fetchRenderer{} })
+	registry.register(tools.FetchToolName, func() renderer { return simpleFetchRenderer{} })
+	registry.register(tools.AgenticFetchToolName, func() renderer { return agenticFetchRenderer{} })
+	registry.register(tools.WebFetchToolName, func() renderer { return webFetchRenderer{} })
 	registry.register(tools.GlobToolName, func() renderer { return globRenderer{} })
 	registry.register(tools.GrepToolName, func() renderer { return grepRenderer{} })
 	registry.register(tools.LSToolName, func() renderer { return lsRenderer{} })
@@ -407,13 +409,13 @@ func (wr writeRenderer) Render(v *toolCallCmp) string {
 //  Fetch renderer
 // -----------------------------------------------------------------------------
 
-// fetchRenderer handles URL fetching with format-specific content display
-type fetchRenderer struct {
+// simpleFetchRenderer handles URL fetching with format-specific content display
+type simpleFetchRenderer struct {
 	baseRenderer
 }
 
 // Render displays the fetched URL with format and timeout parameters
-func (fr fetchRenderer) Render(v *toolCallCmp) string {
+func (fr simpleFetchRenderer) Render(v *toolCallCmp) string {
 	var params tools.FetchParams
 	var args []string
 	if err := fr.unmarshalParams(v.call.Input, &params); err == nil {
@@ -431,7 +433,7 @@ func (fr fetchRenderer) Render(v *toolCallCmp) string {
 }
 
 // getFileExtension returns appropriate file extension for syntax highlighting
-func (fr fetchRenderer) getFileExtension(format string) string {
+func (fr simpleFetchRenderer) getFileExtension(format string) string {
 	switch format {
 	case "text":
 		return "fetch.txt"
@@ -442,6 +444,78 @@ func (fr fetchRenderer) getFileExtension(format string) string {
 	}
 }
 
+// -----------------------------------------------------------------------------
+//  Agentic fetch renderer
+// -----------------------------------------------------------------------------
+
+// agenticFetchRenderer handles URL fetching with prompt parameter and nested tool calls
+type agenticFetchRenderer struct {
+	baseRenderer
+}
+
+// Render displays the fetched URL with prompt parameter and nested tool calls
+func (fr agenticFetchRenderer) Render(v *toolCallCmp) string {
+	t := styles.CurrentTheme()
+	var params tools.AgenticFetchParams
+	var args []string
+	if err := fr.unmarshalParams(v.call.Input, &params); err == nil {
+		args = newParamBuilder().
+			addMain(params.URL).
+			build()
+	}
+
+	prompt := params.Prompt
+	prompt = strings.ReplaceAll(prompt, "\n", " ")
+
+	header := fr.makeHeader(v, "Agentic Fetch", v.textWidth(), args...)
+	if res, done := earlyState(header, v); v.cancelled && done {
+		return res
+	}
+
+	taskTag := t.S().Base.Bold(true).Padding(0, 1).MarginLeft(2).Background(t.GreenLight).Foreground(t.Border).Render("Prompt")
+	remainingWidth := v.textWidth() - (lipgloss.Width(taskTag) + 1)
+	remainingWidth = min(remainingWidth, 120-(lipgloss.Width(taskTag)+1))
+	prompt = t.S().Base.Width(remainingWidth).Render(prompt)
+	header = lipgloss.JoinVertical(
+		lipgloss.Left,
+		header,
+		"",
+		lipgloss.JoinHorizontal(
+			lipgloss.Left,
+			taskTag,
+			" ",
+			prompt,
+		),
+	)
+	childTools := tree.Root(header)
+
+	for _, call := range v.nestedToolCalls {
+		call.SetSize(remainingWidth, 1)
+		childTools.Child(call.View())
+	}
+	parts := []string{
+		childTools.Enumerator(RoundedEnumeratorWithWidth(2, lipgloss.Width(taskTag)-5)).String(),
+	}
+
+	if v.result.ToolCallID == "" {
+		v.spinning = true
+		parts = append(parts, "", v.anim.View())
+	} else {
+		v.spinning = false
+	}
+
+	header = lipgloss.JoinVertical(
+		lipgloss.Left,
+		parts...,
+	)
+
+	if v.result.ToolCallID == "" {
+		return header
+	}
+	body := renderMarkdownContent(v, v.result.Content)
+	return joinHeaderBody(header, body)
+}
+
 // formatTimeout converts timeout seconds to duration string
 func formatTimeout(timeout int) string {
 	if timeout == 0 {
@@ -450,6 +524,30 @@ func formatTimeout(timeout int) string {
 	return (time.Duration(timeout) * time.Second).String()
 }
 
+// -----------------------------------------------------------------------------
+//  Web fetch renderer
+// -----------------------------------------------------------------------------
+
+// webFetchRenderer handles web page fetching with simplified URL display
+type webFetchRenderer struct {
+	baseRenderer
+}
+
+// Render displays a compact view of web_fetch with just the URL in a link style
+func (wfr webFetchRenderer) Render(v *toolCallCmp) string {
+	var params tools.WebFetchParams
+	var args []string
+	if err := wfr.unmarshalParams(v.call.Input, &params); err == nil {
+		args = newParamBuilder().
+			addMain(params.URL).
+			build()
+	}
+
+	return wfr.renderWithParams(v, "Fetch", args, func() string {
+		return renderMarkdownContent(v, v.result.Content)
+	})
+}
+
 // -----------------------------------------------------------------------------
 //  Download renderer
 // -----------------------------------------------------------------------------
@@ -609,11 +707,21 @@ type agentRenderer struct {
 	baseRenderer
 }
 
-func RoundedEnumerator(children tree.Children, index int) string {
-	if children.Length()-1 == index {
-		return " ╰──"
+func RoundedEnumeratorWithWidth(lPadding, width int) tree.Enumerator {
+	if width == 0 {
+		width = 2
+	}
+	if lPadding == 0 {
+		lPadding = 1
+	}
+	return func(children tree.Children, index int) string {
+		line := strings.Repeat("─", width)
+		padding := strings.Repeat(" ", lPadding)
+		if children.Length()-1 == index {
+			return padding + "╰" + line
+		}
+		return padding + "├" + line
 	}
-	return " ├──"
 }
 
 // Render displays agent task parameters and result content
@@ -629,8 +737,9 @@ func (tr agentRenderer) Render(v *toolCallCmp) string {
 	if res, done := earlyState(header, v); v.cancelled && done {
 		return res
 	}
-	taskTag := t.S().Base.Padding(0, 1).MarginLeft(1).Background(t.BlueLight).Foreground(t.White).Render("Task")
-	remainingWidth := v.textWidth() - lipgloss.Width(header) - lipgloss.Width(taskTag) - 2 // -2 for padding
+	taskTag := t.S().Base.Bold(true).Padding(0, 1).MarginLeft(2).Background(t.BlueLight).Foreground(t.White).Render("Task")
+	remainingWidth := v.textWidth() - lipgloss.Width(header) - lipgloss.Width(taskTag) - 2
+	remainingWidth = min(remainingWidth, 120-lipgloss.Width(taskTag)-2)
 	prompt = t.S().Muted.Width(remainingWidth).Render(prompt)
 	header = lipgloss.JoinVertical(
 		lipgloss.Left,
@@ -646,10 +755,11 @@ func (tr agentRenderer) Render(v *toolCallCmp) string {
 	childTools := tree.Root(header)
 
 	for _, call := range v.nestedToolCalls {
+		call.SetSize(remainingWidth, 1)
 		childTools.Child(call.View())
 	}
 	parts := []string{
-		childTools.Enumerator(RoundedEnumerator).String(),
+		childTools.Enumerator(RoundedEnumeratorWithWidth(2, lipgloss.Width(taskTag)-5)).String(),
 	}
 
 	if v.result.ToolCallID == "" {
@@ -668,7 +778,7 @@ func (tr agentRenderer) Render(v *toolCallCmp) string {
 		return header
 	}
 
-	body := renderPlainContent(v, v.result.Content)
+	body := renderMarkdownContent(v, v.result.Content)
 	return joinHeaderBody(header, body)
 }
 
@@ -684,9 +794,6 @@ func renderParamList(nested bool, paramsWidth int, params ...string) string {
 	}
 
 	if len(params) == 1 {
-		if nested {
-			return t.S().Muted.Render(mainParam)
-		}
 		return t.S().Subtle.Render(mainParam)
 	}
 	otherParams := params[1:]
@@ -708,9 +815,6 @@ func renderParamList(nested bool, paramsWidth int, params ...string) string {
 	partsRendered := strings.Join(parts, ", ")
 	remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3 // count for " ()"
 	if remainingWidth < 30 {
-		if nested {
-			return t.S().Muted.Render(mainParam)
-		}
 		// No space for the params, just show the main
 		return t.S().Subtle.Render(mainParam)
 	}
@@ -719,9 +823,6 @@ func renderParamList(nested bool, paramsWidth int, params ...string) string {
 		mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
 	}
 
-	if nested {
-		return t.S().Muted.Render(ansi.Truncate(mainParam, paramsWidth, "…"))
-	}
 	return t.S().Subtle.Render(ansi.Truncate(mainParam, paramsWidth, "…"))
 }
 
@@ -764,14 +865,14 @@ func renderPlainContent(v *toolCallCmp, content string) string {
 	content = strings.TrimSpace(content)
 	lines := strings.Split(content, "\n")
 
-	width := v.textWidth() - 2 // -2 for left padding
+	width := v.textWidth() - 2
 	var out []string
 	for i, ln := range lines {
 		if i >= responseContextHeight {
 			break
 		}
 		ln = ansiext.Escape(ln)
-		ln = " " + ln // left padding
+		ln = " " + ln
 		if len(ln) > width {
 			ln = v.fit(ln, width)
 		}
@@ -791,6 +892,41 @@ func renderPlainContent(v *toolCallCmp, content string) string {
 	return strings.Join(out, "\n")
 }
 
+func renderMarkdownContent(v *toolCallCmp, content string) string {
+	t := styles.CurrentTheme()
+	content = strings.ReplaceAll(content, "\r\n", "\n")
+	content = strings.ReplaceAll(content, "\t", "    ")
+	content = strings.TrimSpace(content)
+
+	width := v.textWidth() - 2
+	width = min(width, 120)
+
+	renderer := styles.GetPlainMarkdownRenderer(width)
+	rendered, err := renderer.Render(content)
+	if err != nil {
+		return renderPlainContent(v, content)
+	}
+
+	lines := strings.Split(rendered, "\n")
+
+	var out []string
+	for i, ln := range lines {
+		if i >= responseContextHeight {
+			break
+		}
+		out = append(out, ln)
+	}
+
+	style := t.S().Muted.Background(t.BgBaseLighter)
+	if len(lines) > responseContextHeight {
+		out = append(out, style.
+			Width(width-2).
+			Render(fmt.Sprintf("… (%d lines)", len(lines)-responseContextHeight)))
+	}
+
+	return style.Render(strings.Join(out, "\n"))
+}
+
 func getDigits(n int) int {
 	if n == 0 {
 		return 1
@@ -885,6 +1021,10 @@ func prettifyToolName(name string) string {
 		return "Multi-Edit"
 	case tools.FetchToolName:
 		return "Fetch"
+	case tools.AgenticFetchToolName:
+		return "Agentic Fetch"
+	case tools.WebFetchToolName:
+		return "Fetching"
 	case tools.GlobToolName:
 		return "Glob"
 	case tools.GrepToolName:

internal/tui/components/chat/messages/tool.go 🔗

@@ -7,9 +7,10 @@ import (
 	"strings"
 	"time"
 
+	"charm.land/bubbles/v2/key"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/atotto/clipboard"
-	"github.com/charmbracelet/bubbles/v2/key"
-	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/crush/internal/agent"
 	"github.com/charmbracelet/crush/internal/agent/tools"
 	"github.com/charmbracelet/crush/internal/diff"
@@ -20,7 +21,6 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/x/ansi"
 )
 
@@ -293,10 +293,25 @@ func (m *toolCallCmp) formatParametersForCopy() string {
 				parts = append(parts, fmt.Sprintf("**Format:** %s", params.Format))
 			}
 			if params.Timeout > 0 {
-				parts = append(parts, fmt.Sprintf("**Timeout:** %s", (time.Duration(params.Timeout)*time.Second).String()))
+				parts = append(parts, fmt.Sprintf("**Timeout:** %ds", params.Timeout))
+			}
+			return strings.Join(parts, "\n")
+		}
+	case tools.AgenticFetchToolName:
+		var params tools.AgenticFetchParams
+		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
+			var parts []string
+			parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
+			if params.Prompt != "" {
+				parts = append(parts, fmt.Sprintf("**Prompt:** %s", params.Prompt))
 			}
 			return strings.Join(parts, "\n")
 		}
+	case tools.WebFetchToolName:
+		var params tools.WebFetchParams
+		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
+			return fmt.Sprintf("**URL:** %s", params.URL)
+		}
 	case tools.GrepToolName:
 		var params tools.GrepParams
 		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
@@ -395,6 +410,10 @@ func (m *toolCallCmp) formatResultForCopy() string {
 		return m.formatWriteResultForCopy()
 	case tools.FetchToolName:
 		return m.formatFetchResultForCopy()
+	case tools.AgenticFetchToolName:
+		return m.formatAgenticFetchResultForCopy()
+	case tools.WebFetchToolName:
+		return m.formatWebFetchResultForCopy()
 	case agent.AgentToolName:
 		return m.formatAgentResultForCopy()
 	case tools.DownloadToolName, tools.GrepToolName, tools.GlobToolName, tools.LSToolName, tools.SourcegraphToolName, tools.DiagnosticsToolName:
@@ -608,15 +627,49 @@ func (m *toolCallCmp) formatFetchResultForCopy() string {
 	if params.URL != "" {
 		result.WriteString(fmt.Sprintf("URL: %s\n", params.URL))
 	}
+	if params.Format != "" {
+		result.WriteString(fmt.Sprintf("Format: %s\n", params.Format))
+	}
+	if params.Timeout > 0 {
+		result.WriteString(fmt.Sprintf("Timeout: %ds\n", params.Timeout))
+	}
+	result.WriteString("\n")
 
-	switch params.Format {
-	case "html":
-		result.WriteString("```html\n")
-	case "text":
-		result.WriteString("```\n")
-	default: // markdown
-		result.WriteString("```markdown\n")
+	result.WriteString(m.result.Content)
+
+	return result.String()
+}
+
+func (m *toolCallCmp) formatAgenticFetchResultForCopy() string {
+	var params tools.AgenticFetchParams
+	if json.Unmarshal([]byte(m.call.Input), &params) != nil {
+		return m.result.Content
 	}
+
+	var result strings.Builder
+	if params.URL != "" {
+		result.WriteString(fmt.Sprintf("URL: %s\n", params.URL))
+	}
+	if params.Prompt != "" {
+		result.WriteString(fmt.Sprintf("Prompt: %s\n\n", params.Prompt))
+	}
+
+	result.WriteString("```markdown\n")
+	result.WriteString(m.result.Content)
+	result.WriteString("\n```")
+
+	return result.String()
+}
+
+func (m *toolCallCmp) formatWebFetchResultForCopy() string {
+	var params tools.WebFetchParams
+	if json.Unmarshal([]byte(m.call.Input), &params) != nil {
+		return m.result.Content
+	}
+
+	var result strings.Builder
+	result.WriteString(fmt.Sprintf("URL: %s\n\n", params.URL))
+	result.WriteString("```markdown\n")
 	result.WriteString(m.result.Content)
 	result.WriteString("\n```")
 
@@ -718,10 +771,10 @@ func (m *toolCallCmp) style() lipgloss.Style {
 	if m.isNested {
 		return t.S().Muted
 	}
-	style := t.S().Muted.PaddingLeft(4)
+	style := t.S().Muted.PaddingLeft(2)
 
 	if m.focused {
-		style = style.PaddingLeft(3).BorderStyle(focusedMessageBorder).BorderLeft(true).BorderForeground(t.GreenDark)
+		style = style.PaddingLeft(1).BorderStyle(focusedMessageBorder).BorderLeft(true).BorderForeground(t.GreenDark)
 	}
 	return style
 }

internal/tui/components/chat/queue.go 🔗

@@ -4,8 +4,8 @@ import (
 	"fmt"
 	"strings"
 
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/tui/styles"
-	"github.com/charmbracelet/lipgloss/v2"
 )
 
 func queuePill(queue int, t *styles.Theme) string {

internal/tui/components/chat/sidebar/sidebar.go 🔗

@@ -6,7 +6,8 @@ import (
 	"slices"
 	"strings"
 
-	tea "github.com/charmbracelet/bubbletea/v2"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/catwalk/pkg/catwalk"
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/csync"
@@ -27,7 +28,6 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
 	"github.com/charmbracelet/crush/internal/version"
-	"github.com/charmbracelet/lipgloss/v2"
 	"golang.org/x/text/cases"
 	"golang.org/x/text/language"
 )

internal/tui/components/chat/splash/splash.go 🔗

@@ -5,9 +5,10 @@ import (
 	"strings"
 	"time"
 
-	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/bubbles/v2/spinner"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/key"
+	"charm.land/bubbles/v2/spinner"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/catwalk/pkg/catwalk"
 	"github.com/charmbracelet/crush/internal/agent"
 	"github.com/charmbracelet/crush/internal/config"
@@ -23,7 +24,6 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
 	"github.com/charmbracelet/crush/internal/version"
-	"github.com/charmbracelet/lipgloss/v2"
 )
 
 type Splash interface {

internal/tui/components/completions/completions.go 🔗

@@ -3,12 +3,12 @@ package completions
 import (
 	"strings"
 
-	"github.com/charmbracelet/bubbles/v2/key"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/key"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/tui/exp/list"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 )
 
 const maxCompletionsHeight = 10

internal/tui/components/core/core.go 🔗

@@ -4,12 +4,12 @@ import (
 	"image/color"
 	"strings"
 
+	"charm.land/bubbles/v2/help"
+	"charm.land/bubbles/v2/key"
+	"charm.land/lipgloss/v2"
 	"github.com/alecthomas/chroma/v2"
-	"github.com/charmbracelet/bubbles/v2/help"
-	"github.com/charmbracelet/bubbles/v2/key"
 	"github.com/charmbracelet/crush/internal/tui/exp/diffview"
 	"github.com/charmbracelet/crush/internal/tui/styles"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/x/ansi"
 )
 

internal/tui/components/core/layout/layout.go 🔗

@@ -1,8 +1,8 @@
 package layout
 
 import (
-	"github.com/charmbracelet/bubbles/v2/key"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/key"
+	tea "charm.land/bubbletea/v2"
 )
 
 // TODO: move this to core

internal/tui/components/core/status/status.go 🔗

@@ -3,11 +3,11 @@ package status
 import (
 	"time"
 
-	"github.com/charmbracelet/bubbles/v2/help"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/help"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/x/ansi"
 )
 

internal/tui/components/dialogs/commands/arguments.go 🔗

@@ -1,17 +1,16 @@
 package commands
 
 import (
-	"fmt"
-	"strings"
+	"cmp"
 
-	"github.com/charmbracelet/bubbles/v2/help"
-	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/bubbles/v2/textinput"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/help"
+	"charm.land/bubbles/v2/key"
+	"charm.land/bubbles/v2/textinput"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/tui/components/dialogs"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 )
 
 const (
@@ -20,9 +19,10 @@ const (
 
 // ShowArgumentsDialogMsg is a message that is sent to show the arguments dialog.
 type ShowArgumentsDialogMsg struct {
-	CommandID string
-	Content   string
-	ArgNames  []string
+	CommandID   string
+	Description string
+	ArgNames    []string
+	OnSubmit    func(args map[string]string) tea.Cmd
 }
 
 // CloseArgumentsDialogMsg is a message that is sent when the arguments dialog is closed.
@@ -39,26 +39,39 @@ type CommandArgumentsDialog interface {
 }
 
 type commandArgumentsDialogCmp struct {
-	width   int
-	wWidth  int // Width of the terminal window
-	wHeight int // Height of the terminal window
-
-	inputs     []textinput.Model
-	focusIndex int
-	keys       ArgumentsDialogKeyMap
-	commandID  string
-	content    string
-	argNames   []string
-	help       help.Model
+	wWidth, wHeight int
+	width, height   int
+
+	inputs    []textinput.Model
+	focused   int
+	keys      ArgumentsDialogKeyMap
+	arguments []Argument
+	help      help.Model
+
+	id          string
+	title       string
+	name        string
+	description string
+
+	onSubmit func(args map[string]string) tea.Cmd
+}
+
+type Argument struct {
+	Name, Title, Description string
+	Required                 bool
 }
 
-func NewCommandArgumentsDialog(commandID, content string, argNames []string) CommandArgumentsDialog {
+func NewCommandArgumentsDialog(
+	id, title, name, description string,
+	arguments []Argument,
+	onSubmit func(args map[string]string) tea.Cmd,
+) CommandArgumentsDialog {
 	t := styles.CurrentTheme()
-	inputs := make([]textinput.Model, len(argNames))
+	inputs := make([]textinput.Model, len(arguments))
 
-	for i, name := range argNames {
+	for i, arg := range arguments {
 		ti := textinput.New()
-		ti.Placeholder = fmt.Sprintf("Enter value for %s...", name)
+		ti.Placeholder = cmp.Or(arg.Description, "Enter value for "+arg.Title)
 		ti.SetWidth(40)
 		ti.SetVirtualCursor(false)
 		ti.Prompt = ""
@@ -75,14 +88,16 @@ func NewCommandArgumentsDialog(commandID, content string, argNames []string) Com
 	}
 
 	return &commandArgumentsDialogCmp{
-		inputs:     inputs,
-		keys:       DefaultArgumentsDialogKeyMap(),
-		commandID:  commandID,
-		content:    content,
-		argNames:   argNames,
-		focusIndex: 0,
-		width:      60,
-		help:       help.New(),
+		inputs:      inputs,
+		keys:        DefaultArgumentsDialogKeyMap(),
+		id:          id,
+		name:        name,
+		title:       title,
+		description: description,
+		arguments:   arguments,
+		width:       60,
+		help:        help.New(),
+		onSubmit:    onSubmit,
 	}
 }
 
@@ -97,47 +112,51 @@ func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
 	case tea.WindowSizeMsg:
 		c.wWidth = msg.Width
 		c.wHeight = msg.Height
+		c.width = min(90, c.wWidth)
+		c.height = min(15, c.wHeight)
+		for i := range c.inputs {
+			c.inputs[i].SetWidth(c.width - (paddingHorizontal * 2))
+		}
 	case tea.KeyPressMsg:
 		switch {
+		case key.Matches(msg, c.keys.Close):
+			return c, util.CmdHandler(dialogs.CloseDialogMsg{})
 		case key.Matches(msg, c.keys.Confirm):
-			if c.focusIndex == len(c.inputs)-1 {
-				content := c.content
-				for i, name := range c.argNames {
+			if c.focused == len(c.inputs)-1 {
+				args := make(map[string]string)
+				for i, arg := range c.arguments {
 					value := c.inputs[i].Value()
-					placeholder := "$" + name
-					content = strings.ReplaceAll(content, placeholder, value)
+					args[arg.Name] = value
 				}
 				return c, tea.Sequence(
 					util.CmdHandler(dialogs.CloseDialogMsg{}),
-					util.CmdHandler(CommandRunCustomMsg{
-						Content: content,
-					}),
+					c.onSubmit(args),
 				)
 			}
 			// Otherwise, move to the next input
-			c.inputs[c.focusIndex].Blur()
-			c.focusIndex++
-			c.inputs[c.focusIndex].Focus()
+			c.inputs[c.focused].Blur()
+			c.focused++
+			c.inputs[c.focused].Focus()
 		case key.Matches(msg, c.keys.Next):
 			// Move to the next input
-			c.inputs[c.focusIndex].Blur()
-			c.focusIndex = (c.focusIndex + 1) % len(c.inputs)
-			c.inputs[c.focusIndex].Focus()
+			c.inputs[c.focused].Blur()
+			c.focused = (c.focused + 1) % len(c.inputs)
+			c.inputs[c.focused].Focus()
 		case key.Matches(msg, c.keys.Previous):
 			// Move to the previous input
-			c.inputs[c.focusIndex].Blur()
-			c.focusIndex = (c.focusIndex - 1 + len(c.inputs)) % len(c.inputs)
-			c.inputs[c.focusIndex].Focus()
+			c.inputs[c.focused].Blur()
+			c.focused = (c.focused - 1 + len(c.inputs)) % len(c.inputs)
+			c.inputs[c.focused].Focus()
 		case key.Matches(msg, c.keys.Close):
 			return c, util.CmdHandler(dialogs.CloseDialogMsg{})
 		default:
 			var cmd tea.Cmd
-			c.inputs[c.focusIndex], cmd = c.inputs[c.focusIndex].Update(msg)
+			c.inputs[c.focused], cmd = c.inputs[c.focused].Update(msg)
 			return c, cmd
 		}
 	case tea.PasteMsg:
 		var cmd tea.Cmd
-		c.inputs[c.focusIndex], cmd = c.inputs[c.focusIndex].Update(msg)
+		c.inputs[c.focused], cmd = c.inputs[c.focused].Update(msg)
 		return c, cmd
 	}
 	return c, nil
@@ -152,26 +171,28 @@ func (c *commandArgumentsDialogCmp) View() string {
 		Foreground(t.Primary).
 		Bold(true).
 		Padding(0, 1).
-		Render("Command Arguments")
+		Render(cmp.Or(c.title, c.name))
 
-	explanation := t.S().Text.
+	promptName := t.S().Text.
 		Padding(0, 1).
-		Render("This command requires arguments.")
+		Render(c.description)
 
-	// Create input fields for each argument
 	inputFields := make([]string, len(c.inputs))
 	for i, input := range c.inputs {
-		// Highlight the label of the focused input
-		labelStyle := baseStyle.
-			Padding(1, 1, 0, 1)
+		labelStyle := baseStyle.Padding(1, 1, 0, 1)
 
-		if i == c.focusIndex {
+		if i == c.focused {
 			labelStyle = labelStyle.Foreground(t.FgBase).Bold(true)
 		} else {
 			labelStyle = labelStyle.Foreground(t.FgMuted)
 		}
 
-		label := labelStyle.Render(c.argNames[i] + ":")
+		arg := c.arguments[i]
+		argName := cmp.Or(arg.Title, arg.Name)
+		if arg.Required {
+			argName += "*"
+		}
+		label := labelStyle.Render(argName + ":")
 
 		field := t.S().Text.
 			Padding(0, 1).
@@ -180,18 +201,14 @@ func (c *commandArgumentsDialogCmp) View() string {
 		inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
 	}
 
-	// Join all elements vertically
-	elements := []string{title, explanation}
+	elements := []string{title, promptName}
 	elements = append(elements, inputFields...)
 
 	c.help.ShowAll = false
 	helpText := baseStyle.Padding(0, 1).Render(c.help.View(c.keys))
 	elements = append(elements, "", helpText)
 
-	content := lipgloss.JoinVertical(
-		lipgloss.Left,
-		elements...,
-	)
+	content := lipgloss.JoinVertical(lipgloss.Left, elements...)
 
 	return baseStyle.Padding(1, 1, 0, 1).
 		Border(lipgloss.RoundedBorder()).
@@ -201,26 +218,33 @@ func (c *commandArgumentsDialogCmp) View() string {
 }
 
 func (c *commandArgumentsDialogCmp) Cursor() *tea.Cursor {
-	cursor := c.inputs[c.focusIndex].Cursor()
+	if len(c.inputs) == 0 {
+		return nil
+	}
+	cursor := c.inputs[c.focused].Cursor()
 	if cursor != nil {
 		cursor = c.moveCursor(cursor)
 	}
 	return cursor
 }
 
+const (
+	headerHeight      = 3
+	itemHeight        = 3
+	paddingHorizontal = 3
+)
+
 func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
 	row, col := c.Position()
-	offset := row + 3 + (1+c.focusIndex)*3
+	offset := row + headerHeight + (1+c.focused)*itemHeight
 	cursor.Y += offset
-	cursor.X = cursor.X + col + 3
+	cursor.X = cursor.X + col + paddingHorizontal
 	return cursor
 }
 
 func (c *commandArgumentsDialogCmp) Position() (int, int) {
-	row := c.wHeight / 2
-	row -= c.wHeight / 2
-	col := c.wWidth / 2
-	col -= c.width / 2
+	row := (c.wHeight / 2) - (c.height / 2)
+	col := (c.wWidth / 2) - (c.width / 2)
 	return row, col
 }
 

internal/tui/components/dialogs/commands/commands.go 🔗

@@ -2,15 +2,20 @@ package commands
 
 import (
 	"os"
+	"slices"
+	"strings"
 
-	"github.com/charmbracelet/bubbles/v2/help"
-	"github.com/charmbracelet/bubbles/v2/key"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/help"
+	"charm.land/bubbles/v2/key"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/catwalk/pkg/catwalk"
-	"github.com/charmbracelet/lipgloss/v2"
 
 	"github.com/charmbracelet/crush/internal/agent"
+	"github.com/charmbracelet/crush/internal/agent/tools/mcp"
 	"github.com/charmbracelet/crush/internal/config"
+	"github.com/charmbracelet/crush/internal/csync"
+	"github.com/charmbracelet/crush/internal/pubsub"
 	"github.com/charmbracelet/crush/internal/tui/components/chat"
 	"github.com/charmbracelet/crush/internal/tui/components/core"
 	"github.com/charmbracelet/crush/internal/tui/components/dialogs"
@@ -25,9 +30,14 @@ const (
 	defaultWidth int = 70
 )
 
+type commandType uint
+
+func (c commandType) String() string { return []string{"System", "User", "MCP"}[c] }
+
 const (
-	SystemCommands int = iota
+	SystemCommands commandType = iota
 	UserCommands
+	MCPPrompts
 )
 
 type listModel = list.FilterableList[list.CompletionItem[Command]]
@@ -54,9 +64,10 @@ type commandDialogCmp struct {
 	commandList  listModel
 	keyMap       CommandsDialogKeyMap
 	help         help.Model
-	commandType  int       // SystemCommands or UserCommands
-	userCommands []Command // User-defined commands
-	sessionID    string    // Current session ID
+	selected     commandType           // Selected SystemCommands, UserCommands, or MCPPrompts
+	userCommands []Command             // User-defined commands
+	mcpPrompts   *csync.Slice[Command] // MCP prompts
+	sessionID    string                // Current session ID
 }
 
 type (
@@ -102,8 +113,9 @@ func NewCommandDialog(sessionID string) CommandsDialog {
 		width:       defaultWidth,
 		keyMap:      DefaultCommandsDialogKeyMap(),
 		help:        help,
-		commandType: SystemCommands,
+		selected:    SystemCommands,
 		sessionID:   sessionID,
+		mcpPrompts:  csync.NewSlice[Command](),
 	}
 }
 
@@ -113,7 +125,8 @@ func (c *commandDialogCmp) Init() tea.Cmd {
 		return util.ReportError(err)
 	}
 	c.userCommands = commands
-	return c.SetCommandType(c.commandType)
+	c.mcpPrompts.SetSlice(loadMCPPrompts())
+	return c.setCommandType(c.selected)
 }
 
 func (c *commandDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
@@ -122,9 +135,19 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
 		c.wWidth = msg.Width
 		c.wHeight = msg.Height
 		return c, tea.Batch(
-			c.SetCommandType(c.commandType),
+			c.setCommandType(c.selected),
 			c.commandList.SetSize(c.listWidth(), c.listHeight()),
 		)
+	case pubsub.Event[mcp.Event]:
+		// Reload MCP prompts when MCP state changes
+		if msg.Type == pubsub.UpdatedEvent {
+			c.mcpPrompts.SetSlice(loadMCPPrompts())
+			// If we're currently viewing MCP prompts, refresh the list
+			if c.selected == MCPPrompts {
+				return c, c.setCommandType(MCPPrompts)
+			}
+			return c, nil
+		}
 	case tea.KeyPressMsg:
 		switch {
 		case key.Matches(msg, c.keyMap.Select):
@@ -138,15 +161,10 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
 				command.Handler(command),
 			)
 		case key.Matches(msg, c.keyMap.Tab):
-			if len(c.userCommands) == 0 {
+			if len(c.userCommands) == 0 && c.mcpPrompts.Len() == 0 {
 				return c, nil
 			}
-			// Toggle command type between System and User commands
-			if c.commandType == SystemCommands {
-				return c, c.SetCommandType(UserCommands)
-			} else {
-				return c, c.SetCommandType(SystemCommands)
-			}
+			return c, c.setCommandType(c.next())
 		case key.Matches(msg, c.keyMap.Close):
 			return c, util.CmdHandler(dialogs.CloseDialogMsg{})
 		default:
@@ -158,13 +176,35 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
 	return c, nil
 }
 
+func (c *commandDialogCmp) next() commandType {
+	switch c.selected {
+	case SystemCommands:
+		if len(c.userCommands) > 0 {
+			return UserCommands
+		}
+		if c.mcpPrompts.Len() > 0 {
+			return MCPPrompts
+		}
+		fallthrough
+	case UserCommands:
+		if c.mcpPrompts.Len() > 0 {
+			return MCPPrompts
+		}
+		fallthrough
+	case MCPPrompts:
+		return SystemCommands
+	default:
+		return SystemCommands
+	}
+}
+
 func (c *commandDialogCmp) View() string {
 	t := styles.CurrentTheme()
 	listView := c.commandList
 	radio := c.commandTypeRadio()
 
 	header := t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Commands", c.width-lipgloss.Width(radio)-5) + " " + radio)
-	if len(c.userCommands) == 0 {
+	if len(c.userCommands) == 0 && c.mcpPrompts.Len() == 0 {
 		header = t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Commands", c.width-4))
 	}
 	content := lipgloss.JoinVertical(
@@ -190,27 +230,41 @@ func (c *commandDialogCmp) Cursor() *tea.Cursor {
 
 func (c *commandDialogCmp) commandTypeRadio() string {
 	t := styles.CurrentTheme()
-	choices := []string{"System", "User"}
-	iconSelected := "◉"
-	iconUnselected := "○"
-	if c.commandType == SystemCommands {
-		return t.S().Base.Foreground(t.FgHalfMuted).Render(iconSelected + " " + choices[0] + " " + iconUnselected + " " + choices[1])
+
+	fn := func(i commandType) string {
+		if i == c.selected {
+			return "◉ " + i.String()
+		}
+		return "○ " + i.String()
+	}
+
+	parts := []string{
+		fn(SystemCommands),
+	}
+	if len(c.userCommands) > 0 {
+		parts = append(parts, fn(UserCommands))
+	}
+	if c.mcpPrompts.Len() > 0 {
+		parts = append(parts, fn(MCPPrompts))
 	}
-	return t.S().Base.Foreground(t.FgHalfMuted).Render(iconUnselected + " " + choices[0] + " " + iconSelected + " " + choices[1])
+	return t.S().Base.Foreground(t.FgHalfMuted).Render(strings.Join(parts, " "))
 }
 
 func (c *commandDialogCmp) listWidth() int {
 	return defaultWidth - 2 // 4 for padding
 }
 
-func (c *commandDialogCmp) SetCommandType(commandType int) tea.Cmd {
-	c.commandType = commandType
+func (c *commandDialogCmp) setCommandType(commandType commandType) tea.Cmd {
+	c.selected = commandType
 
 	var commands []Command
-	if c.commandType == SystemCommands {
+	switch c.selected {
+	case SystemCommands:
 		commands = c.defaultCommands()
-	} else {
+	case UserCommands:
 		commands = c.userCommands
+	case MCPPrompts:
+		commands = slices.Collect(c.mcpPrompts.Seq())
 	}
 
 	commandItems := []list.CompletionItem[Command]{}

internal/tui/components/dialogs/commands/loader.go 🔗

@@ -1,6 +1,8 @@
 package commands
 
 import (
+	"cmp"
+	"context"
 	"fmt"
 	"io/fs"
 	"os"
@@ -8,15 +10,17 @@ import (
 	"regexp"
 	"strings"
 
-	tea "github.com/charmbracelet/bubbletea/v2"
+	tea "charm.land/bubbletea/v2"
+	"github.com/charmbracelet/crush/internal/agent/tools/mcp"
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/home"
+	"github.com/charmbracelet/crush/internal/tui/components/chat"
 	"github.com/charmbracelet/crush/internal/tui/util"
 )
 
 const (
-	UserCommandPrefix    = "user:"
-	ProjectCommandPrefix = "project:"
+	userCommandPrefix    = "user:"
+	projectCommandPrefix = "project:"
 )
 
 var namedArgPattern = regexp.MustCompile(`\$([A-Z][A-Z0-9_]*)`)
@@ -50,7 +54,7 @@ func buildCommandSources(cfg *config.Config) []commandSource {
 	if dir := getXDGCommandsDir(); dir != "" {
 		sources = append(sources, commandSource{
 			path:   dir,
-			prefix: UserCommandPrefix,
+			prefix: userCommandPrefix,
 		})
 	}
 
@@ -58,14 +62,14 @@ func buildCommandSources(cfg *config.Config) []commandSource {
 	if home := home.Dir(); home != "" {
 		sources = append(sources, commandSource{
 			path:   filepath.Join(home, ".crush", "commands"),
-			prefix: UserCommandPrefix,
+			prefix: userCommandPrefix,
 		})
 	}
 
 	// Project directory
 	sources = append(sources, commandSource{
 		path:   filepath.Join(cfg.Options.DataDirectory, "commands"),
-		prefix: ProjectCommandPrefix,
+		prefix: projectCommandPrefix,
 	})
 
 	return sources
@@ -127,12 +131,13 @@ func (l *commandLoader) loadCommand(path, baseDir, prefix string) (Command, erro
 	}
 
 	id := buildCommandID(path, baseDir, prefix)
+	desc := fmt.Sprintf("Custom command from %s", filepath.Base(path))
 
 	return Command{
 		ID:          id,
 		Title:       id,
-		Description: fmt.Sprintf("Custom command from %s", filepath.Base(path)),
-		Handler:     createCommandHandler(id, string(content)),
+		Description: desc,
+		Handler:     createCommandHandler(id, desc, string(content)),
 	}, nil
 }
 
@@ -149,21 +154,35 @@ func buildCommandID(path, baseDir, prefix string) string {
 	return prefix + strings.Join(parts, ":")
 }
 
-func createCommandHandler(id string, content string) func(Command) tea.Cmd {
+func createCommandHandler(id, desc, content string) func(Command) tea.Cmd {
 	return func(cmd Command) tea.Cmd {
 		args := extractArgNames(content)
 
-		if len(args) > 0 {
-			return util.CmdHandler(ShowArgumentsDialogMsg{
-				CommandID: id,
-				Content:   content,
-				ArgNames:  args,
+		if len(args) == 0 {
+			return util.CmdHandler(CommandRunCustomMsg{
+				Content: content,
 			})
 		}
+		return util.CmdHandler(ShowArgumentsDialogMsg{
+			CommandID:   id,
+			Description: desc,
+			ArgNames:    args,
+			OnSubmit: func(args map[string]string) tea.Cmd {
+				return execUserPrompt(content, args)
+			},
+		})
+	}
+}
 
-		return util.CmdHandler(CommandRunCustomMsg{
+func execUserPrompt(content string, args map[string]string) tea.Cmd {
+	return func() tea.Msg {
+		for name, value := range args {
+			placeholder := "$" + name
+			content = strings.ReplaceAll(content, placeholder, value)
+		}
+		return CommandRunCustomMsg{
 			Content: content,
-		})
+		}
 	}
 }
 
@@ -201,3 +220,53 @@ func isMarkdownFile(name string) bool {
 type CommandRunCustomMsg struct {
 	Content string
 }
+
+func loadMCPPrompts() []Command {
+	var commands []Command
+	for mcpName, prompts := range mcp.Prompts() {
+		for _, prompt := range prompts {
+			key := mcpName + ":" + prompt.Name
+			commands = append(commands, Command{
+				ID:          key,
+				Title:       cmp.Or(prompt.Title, prompt.Name),
+				Description: prompt.Description,
+				Handler:     createMCPPromptHandler(mcpName, prompt.Name, prompt),
+			})
+		}
+	}
+
+	return commands
+}
+
+func createMCPPromptHandler(mcpName, promptName string, prompt *mcp.Prompt) func(Command) tea.Cmd {
+	return func(cmd Command) tea.Cmd {
+		if len(prompt.Arguments) == 0 {
+			return execMCPPrompt(mcpName, promptName, nil)
+		}
+		return util.CmdHandler(ShowMCPPromptArgumentsDialogMsg{
+			Prompt: prompt,
+			OnSubmit: func(args map[string]string) tea.Cmd {
+				return execMCPPrompt(mcpName, promptName, args)
+			},
+		})
+	}
+}
+
+func execMCPPrompt(clientName, promptName string, args map[string]string) tea.Cmd {
+	return func() tea.Msg {
+		ctx := context.Background()
+		result, err := mcp.GetPromptMessages(ctx, clientName, promptName, args)
+		if err != nil {
+			return util.ReportError(err)
+		}
+
+		return chat.SendMsg{
+			Text: strings.Join(result, " "),
+		}
+	}
+}
+
+type ShowMCPPromptArgumentsDialogMsg struct {
+	Prompt   *mcp.Prompt
+	OnSubmit func(arg map[string]string) tea.Cmd
+}

internal/tui/components/dialogs/dialogs.go 🔗

@@ -3,9 +3,9 @@ package dialogs
 import (
 	"slices"
 
-	tea "github.com/charmbracelet/bubbletea/v2"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 )
 
 type DialogID string

internal/tui/components/dialogs/filepicker/filepicker.go 🔗

@@ -7,10 +7,11 @@ import (
 	"path/filepath"
 	"strings"
 
-	"github.com/charmbracelet/bubbles/v2/filepicker"
-	"github.com/charmbracelet/bubbles/v2/help"
-	"github.com/charmbracelet/bubbles/v2/key"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/filepicker"
+	"charm.land/bubbles/v2/help"
+	"charm.land/bubbles/v2/key"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/home"
 	"github.com/charmbracelet/crush/internal/message"
 	"github.com/charmbracelet/crush/internal/tui/components/core"
@@ -18,7 +19,6 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/components/image"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 )
 
 const (

internal/tui/components/dialogs/keys.go 🔗

@@ -1,7 +1,7 @@
 package dialogs
 
 import (
-	"github.com/charmbracelet/bubbles/v2/key"
+	"charm.land/bubbles/v2/key"
 )
 
 // KeyMap defines keyboard bindings for dialog management.

internal/tui/components/dialogs/models/apikey.go 🔗

@@ -3,14 +3,14 @@ package models
 import (
 	"fmt"
 
-	"github.com/charmbracelet/bubbles/v2/spinner"
-	"github.com/charmbracelet/bubbles/v2/textinput"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/spinner"
+	"charm.land/bubbles/v2/textinput"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/home"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 )
 
 type APIKeyInputState int

internal/tui/components/dialogs/models/list.go 🔗

@@ -6,7 +6,7 @@ import (
 	"slices"
 	"strings"
 
-	tea "github.com/charmbracelet/bubbletea/v2"
+	tea "charm.land/bubbletea/v2"
 	"github.com/charmbracelet/catwalk/pkg/catwalk"
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/tui/exp/list"

internal/tui/components/dialogs/models/models.go 🔗

@@ -4,10 +4,11 @@ import (
 	"fmt"
 	"time"
 
-	"github.com/charmbracelet/bubbles/v2/help"
-	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/bubbles/v2/spinner"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/help"
+	"charm.land/bubbles/v2/key"
+	"charm.land/bubbles/v2/spinner"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/catwalk/pkg/catwalk"
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/tui/components/core"
@@ -15,7 +16,6 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/exp/list"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 )
 
 const (

internal/tui/components/dialogs/permissions/permissions.go 🔗

@@ -5,10 +5,11 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/charmbracelet/bubbles/v2/help"
-	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/bubbles/v2/viewport"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/help"
+	"charm.land/bubbles/v2/key"
+	"charm.land/bubbles/v2/viewport"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/agent/tools"
 	"github.com/charmbracelet/crush/internal/fsext"
 	"github.com/charmbracelet/crush/internal/permission"
@@ -16,7 +17,6 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/components/dialogs"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/x/ansi"
 )
 
@@ -373,6 +373,8 @@ func (p *permissionDialogCmp) renderHeader() string {
 		)
 	case tools.FetchToolName:
 		headerParts = append(headerParts, t.S().Muted.Width(p.width).Bold(true).Render("URL"))
+	case tools.AgenticFetchToolName:
+		headerParts = append(headerParts, t.S().Muted.Width(p.width).Bold(true).Render("URL"))
 	case tools.ViewToolName:
 		params := p.permission.Params.(tools.ViewPermissionsParams)
 		fileKey := t.S().Muted.Render("File")
@@ -427,6 +429,8 @@ func (p *permissionDialogCmp) getOrGenerateContent() string {
 		content = p.generateMultiEditContent()
 	case tools.FetchToolName:
 		content = p.generateFetchContent()
+	case tools.AgenticFetchToolName:
+		content = p.generateAgenticFetchContent()
 	case tools.ViewToolName:
 		content = p.generateViewContent()
 	case tools.LSToolName:
@@ -570,6 +574,20 @@ func (p *permissionDialogCmp) generateFetchContent() string {
 	return ""
 }
 
+func (p *permissionDialogCmp) generateAgenticFetchContent() string {
+	t := styles.CurrentTheme()
+	baseStyle := t.S().Base.Background(t.BgSubtle)
+	if pr, ok := p.permission.Params.(tools.AgenticFetchPermissionsParams); ok {
+		content := fmt.Sprintf("URL: %s\n\nPrompt: %s", pr.URL, pr.Prompt)
+		finalContent := baseStyle.
+			Padding(1, 2).
+			Width(p.contentViewPort.Width()).
+			Render(content)
+		return finalContent
+	}
+	return ""
+}
+
 func (p *permissionDialogCmp) generateViewContent() string {
 	t := styles.CurrentTheme()
 	baseStyle := t.S().Base.Background(t.BgSubtle)
@@ -775,6 +793,9 @@ func (p *permissionDialogCmp) SetSize() tea.Cmd {
 	case tools.FetchToolName:
 		p.width = int(float64(p.wWidth) * 0.8)
 		p.height = int(float64(p.wHeight) * 0.3)
+	case tools.AgenticFetchToolName:
+		p.width = int(float64(p.wWidth) * 0.8)
+		p.height = int(float64(p.wHeight) * 0.4)
 	case tools.ViewToolName:
 		p.width = int(float64(p.wWidth) * 0.8)
 		p.height = int(float64(p.wHeight) * 0.4)

internal/tui/components/dialogs/quit/quit.go 🔗

@@ -1,12 +1,12 @@
 package quit
 
 import (
-	"github.com/charmbracelet/bubbles/v2/key"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/key"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/tui/components/dialogs"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 )
 
 const (

internal/tui/components/dialogs/reasoning/reasoning.go 🔗

@@ -1,10 +1,10 @@
 package reasoning
 
 import (
-	"github.com/charmbracelet/bubbles/v2/help"
-	"github.com/charmbracelet/bubbles/v2/key"
-	tea "github.com/charmbracelet/bubbletea/v2"
-	"github.com/charmbracelet/lipgloss/v2"
+	"charm.land/bubbles/v2/help"
+	"charm.land/bubbles/v2/key"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"golang.org/x/text/cases"
 	"golang.org/x/text/language"
 

internal/tui/components/dialogs/sessions/sessions.go 🔗

@@ -1,9 +1,10 @@
 package sessions
 
 import (
-	"github.com/charmbracelet/bubbles/v2/help"
-	"github.com/charmbracelet/bubbles/v2/key"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/help"
+	"charm.land/bubbles/v2/key"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/event"
 	"github.com/charmbracelet/crush/internal/session"
 	"github.com/charmbracelet/crush/internal/tui/components/chat"
@@ -12,7 +13,6 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/exp/list"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 )
 
 const SessionsDialogID dialogs.DialogID = "sessions"

internal/tui/components/files/files.go 🔗

@@ -7,7 +7,7 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/charmbracelet/lipgloss/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/x/ansi"
 
 	"github.com/charmbracelet/crush/internal/config"

internal/tui/components/image/load.go 🔗

@@ -11,7 +11,7 @@ import (
 	"os"
 	"strings"
 
-	tea "github.com/charmbracelet/bubbletea/v2"
+	tea "charm.land/bubbletea/v2"
 	"github.com/disintegration/imageorient"
 	"github.com/lucasb-eyer/go-colorful"
 	"github.com/muesli/termenv"

internal/tui/components/logo/logo.go 🔗

@@ -6,9 +6,9 @@ import (
 	"image/color"
 	"strings"
 
+	"charm.land/lipgloss/v2"
 	"github.com/MakeNowJust/heredoc"
 	"github.com/charmbracelet/crush/internal/tui/styles"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/x/ansi"
 	"github.com/charmbracelet/x/exp/slice"
 )

internal/tui/components/lsp/lsp.go 🔗

@@ -4,13 +4,13 @@ import (
 	"fmt"
 	"strings"
 
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/app"
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/csync"
 	"github.com/charmbracelet/crush/internal/lsp"
 	"github.com/charmbracelet/crush/internal/tui/components/core"
 	"github.com/charmbracelet/crush/internal/tui/styles"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
 )
 

internal/tui/components/mcp/mcp.go 🔗

@@ -2,10 +2,11 @@ package mcp
 
 import (
 	"fmt"
+	"strings"
 
-	"github.com/charmbracelet/lipgloss/v2"
+	"charm.land/lipgloss/v2"
 
-	"github.com/charmbracelet/crush/internal/agent/tools"
+	"github.com/charmbracelet/crush/internal/agent/tools/mcp"
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/tui/components/core"
 	"github.com/charmbracelet/crush/internal/tui/styles"
@@ -40,7 +41,7 @@ func RenderMCPList(opts RenderOptions) []string {
 	}
 
 	// Get MCP states
-	mcpStates := tools.GetMCPStates()
+	mcpStates := mcp.GetStates()
 
 	// Determine how many items to show
 	maxItems := len(mcps)
@@ -56,21 +57,24 @@ func RenderMCPList(opts RenderOptions) []string {
 		// Determine icon and color based on state
 		icon := t.ItemOfflineIcon
 		description := ""
-		extraContent := ""
+		extraContent := []string{}
 
 		if state, exists := mcpStates[l.Name]; exists {
 			switch state.State {
-			case tools.MCPStateDisabled:
+			case mcp.StateDisabled:
 				description = t.S().Subtle.Render("disabled")
-			case tools.MCPStateStarting:
+			case mcp.StateStarting:
 				icon = t.ItemBusyIcon
 				description = t.S().Subtle.Render("starting...")
-			case tools.MCPStateConnected:
+			case mcp.StateConnected:
 				icon = t.ItemOnlineIcon
-				if state.ToolCount > 0 {
-					extraContent = t.S().Subtle.Render(fmt.Sprintf("%d tools", state.ToolCount))
+				if count := state.Counts.Tools; count > 0 {
+					extraContent = append(extraContent, t.S().Subtle.Render(fmt.Sprintf("%d tools", count)))
 				}
-			case tools.MCPStateError:
+				if count := state.Counts.Prompts; count > 0 {
+					extraContent = append(extraContent, t.S().Subtle.Render(fmt.Sprintf("%d prompts", count)))
+				}
+			case mcp.StateError:
 				icon = t.ItemErrorIcon
 				if state.Error != nil {
 					description = t.S().Subtle.Render(fmt.Sprintf("error: %s", state.Error.Error()))
@@ -88,7 +92,7 @@ func RenderMCPList(opts RenderOptions) []string {
 					Icon:         icon.String(),
 					Title:        l.Name,
 					Description:  description,
-					ExtraContent: extraContent,
+					ExtraContent: strings.Join(extraContent, " "),
 				},
 				opts.MaxWidth,
 			),

internal/tui/exp/diffview/chroma.go 🔗

@@ -6,9 +6,9 @@ import (
 	"io"
 	"strings"
 
+	"charm.land/lipgloss/v2"
 	"github.com/alecthomas/chroma/v2"
 	"github.com/charmbracelet/crush/internal/ansiext"
-	"github.com/charmbracelet/lipgloss/v2"
 )
 
 var _ chroma.Formatter = chromaFormatter{}

internal/tui/exp/diffview/diffview.go 🔗

@@ -6,10 +6,10 @@ import (
 	"strconv"
 	"strings"
 
+	"charm.land/lipgloss/v2"
 	"github.com/alecthomas/chroma/v2"
 	"github.com/alecthomas/chroma/v2/lexers"
 	"github.com/aymanbagabas/go-udiff"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/x/ansi"
 	"github.com/zeebo/xxh3"
 )

internal/tui/exp/diffview/style.go 🔗

@@ -1,7 +1,7 @@
 package diffview
 
 import (
-	"github.com/charmbracelet/lipgloss/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/x/exp/charmtone"
 )
 

internal/tui/exp/list/filterable.go 🔗

@@ -4,13 +4,13 @@ import (
 	"regexp"
 	"slices"
 
-	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/bubbles/v2/textinput"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/key"
+	"charm.land/bubbles/v2/textinput"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/sahilm/fuzzy"
 )
 

internal/tui/exp/list/filterable_group.go 🔗

@@ -5,13 +5,13 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/bubbles/v2/textinput"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/key"
+	"charm.land/bubbles/v2/textinput"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/sahilm/fuzzy"
 )
 

internal/tui/exp/list/grouped.go 🔗

@@ -1,7 +1,7 @@
 package list
 
 import (
-	tea "github.com/charmbracelet/bubbletea/v2"
+	tea "charm.land/bubbletea/v2"
 	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 	"github.com/charmbracelet/crush/internal/tui/util"
 )

internal/tui/exp/list/items.go 🔗

@@ -3,12 +3,12 @@ package list
 import (
 	"image/color"
 
-	tea "github.com/charmbracelet/bubbletea/v2"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/tui/components/core"
 	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/x/ansi"
 	"github.com/google/uuid"
 	"github.com/rivo/uniseg"

internal/tui/exp/list/keys.go 🔗

@@ -1,7 +1,7 @@
 package list
 
 import (
-	"github.com/charmbracelet/bubbles/v2/key"
+	"charm.land/bubbles/v2/key"
 )
 
 type KeyMap struct {

internal/tui/exp/list/list.go 🔗

@@ -4,13 +4,13 @@ import (
 	"strings"
 	"sync"
 
-	"github.com/charmbracelet/bubbles/v2/key"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/key"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/tui/components/anim"
 	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 	uv "github.com/charmbracelet/ultraviolet"
 	"github.com/charmbracelet/x/ansi"
 	"github.com/charmbracelet/x/exp/ordered"

internal/tui/exp/list/list_test.go 🔗

@@ -5,10 +5,10 @@ import (
 	"strings"
 	"testing"
 
-	tea "github.com/charmbracelet/bubbletea/v2"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/x/exp/golden"
 	"github.com/google/uuid"
 	"github.com/stretchr/testify/assert"

internal/tui/keys.go 🔗

@@ -1,7 +1,7 @@
 package tui
 
 import (
-	"github.com/charmbracelet/bubbles/v2/key"
+	"charm.land/bubbles/v2/key"
 )
 
 type KeyMap struct {

internal/tui/page/chat/chat.go 🔗

@@ -6,10 +6,11 @@ import (
 	"fmt"
 	"time"
 
-	"github.com/charmbracelet/bubbles/v2/help"
-	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/bubbles/v2/spinner"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/help"
+	"charm.land/bubbles/v2/key"
+	"charm.land/bubbles/v2/spinner"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/app"
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/history"
@@ -36,7 +37,6 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
 	"github.com/charmbracelet/crush/internal/version"
-	"github.com/charmbracelet/lipgloss/v2"
 )
 
 var ChatPageID page.PageID = "chat"
@@ -1025,7 +1025,8 @@ func (p *chatPage) Help() help.KeyMap {
 				// to reflect that.
 				key.WithHelp("ctrl+j", "newline"),
 			)
-			if p.keyboardEnhancements.SupportsKeyDisambiguation() {
+			if p.keyboardEnhancements.Flags > 0 {
+				// Non-zero flags mean we have at least key disambiguation.
 				newLineBinding.SetHelp("shift+enter", newLineBinding.Help().Desc)
 			}
 			shortList = append(shortList, newLineBinding)
@@ -1037,8 +1038,8 @@ func (p *chatPage) Help() help.KeyMap {
 						key.WithHelp("ctrl+f", "add image"),
 					),
 					key.NewBinding(
-						key.WithKeys("/"),
-						key.WithHelp("/", "add file"),
+						key.WithKeys("@"),
+						key.WithHelp("@", "mention file"),
 					),
 					key.NewBinding(
 						key.WithKeys("ctrl+o"),

internal/tui/page/chat/keys.go 🔗

@@ -1,7 +1,7 @@
 package chat
 
 import (
-	"github.com/charmbracelet/bubbles/v2/key"
+	"charm.land/bubbles/v2/key"
 )
 
 type KeyMap struct {

internal/tui/styles/charmtone.go 🔗

@@ -1,7 +1,7 @@
 package styles
 
 import (
-	"github.com/charmbracelet/lipgloss/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/x/exp/charmtone"
 )
 

internal/tui/styles/markdown.go 🔗

@@ -1,9 +1,19 @@
 package styles
 
 import (
+	"fmt"
+	"image/color"
+
 	"github.com/charmbracelet/glamour/v2"
+	"github.com/charmbracelet/glamour/v2/ansi"
 )
 
+// lipglossColorToHex converts a color.Color to hex string
+func lipglossColorToHex(c color.Color) string {
+	r, g, b, _ := c.RGBA()
+	return fmt.Sprintf("#%02x%02x%02x", r>>8, g>>8, b>>8)
+}
+
 // Helper functions for style pointers
 func boolPtr(b bool) *bool       { return &b }
 func stringPtr(s string) *string { return &s }
@@ -18,3 +28,178 @@ func GetMarkdownRenderer(width int) *glamour.TermRenderer {
 	)
 	return r
 }
+
+// returns a glamour TermRenderer with no colors (plain text with structure)
+func GetPlainMarkdownRenderer(width int) *glamour.TermRenderer {
+	r, _ := glamour.NewTermRenderer(
+		glamour.WithStyles(PlainMarkdownStyle()),
+		glamour.WithWordWrap(width),
+	)
+	return r
+}
+
+// PlainMarkdownStyle returns a glamour style config with no colors
+func PlainMarkdownStyle() ansi.StyleConfig {
+	t := CurrentTheme()
+	bgColor := stringPtr(lipglossColorToHex(t.BgBaseLighter))
+	fgColor := stringPtr(lipglossColorToHex(t.FgMuted))
+	return ansi.StyleConfig{
+		Document: ansi.StyleBlock{
+			StylePrimitive: ansi.StylePrimitive{
+				Color:           fgColor,
+				BackgroundColor: bgColor,
+			},
+		},
+		BlockQuote: ansi.StyleBlock{
+			StylePrimitive: ansi.StylePrimitive{
+				Color:           fgColor,
+				BackgroundColor: bgColor,
+			},
+			Indent:      uintPtr(1),
+			IndentToken: stringPtr("│ "),
+		},
+		List: ansi.StyleList{
+			LevelIndent: defaultListIndent,
+		},
+		Heading: ansi.StyleBlock{
+			StylePrimitive: ansi.StylePrimitive{
+				BlockSuffix:     "\n",
+				Bold:            boolPtr(true),
+				Color:           fgColor,
+				BackgroundColor: bgColor,
+			},
+		},
+		H1: ansi.StyleBlock{
+			StylePrimitive: ansi.StylePrimitive{
+				Prefix:          " ",
+				Suffix:          " ",
+				Bold:            boolPtr(true),
+				Color:           fgColor,
+				BackgroundColor: bgColor,
+			},
+		},
+		H2: ansi.StyleBlock{
+			StylePrimitive: ansi.StylePrimitive{
+				Prefix:          "## ",
+				Color:           fgColor,
+				BackgroundColor: bgColor,
+			},
+		},
+		H3: ansi.StyleBlock{
+			StylePrimitive: ansi.StylePrimitive{
+				Prefix:          "### ",
+				Color:           fgColor,
+				BackgroundColor: bgColor,
+			},
+		},
+		H4: ansi.StyleBlock{
+			StylePrimitive: ansi.StylePrimitive{
+				Prefix:          "#### ",
+				Color:           fgColor,
+				BackgroundColor: bgColor,
+			},
+		},
+		H5: ansi.StyleBlock{
+			StylePrimitive: ansi.StylePrimitive{
+				Prefix:          "##### ",
+				Color:           fgColor,
+				BackgroundColor: bgColor,
+			},
+		},
+		H6: ansi.StyleBlock{
+			StylePrimitive: ansi.StylePrimitive{
+				Prefix:          "###### ",
+				Color:           fgColor,
+				BackgroundColor: bgColor,
+			},
+		},
+		Strikethrough: ansi.StylePrimitive{
+			CrossedOut:      boolPtr(true),
+			Color:           fgColor,
+			BackgroundColor: bgColor,
+		},
+		Emph: ansi.StylePrimitive{
+			Italic:          boolPtr(true),
+			Color:           fgColor,
+			BackgroundColor: bgColor,
+		},
+		Strong: ansi.StylePrimitive{
+			Bold:            boolPtr(true),
+			Color:           fgColor,
+			BackgroundColor: bgColor,
+		},
+		HorizontalRule: ansi.StylePrimitive{
+			Format:          "\n--------\n",
+			Color:           fgColor,
+			BackgroundColor: bgColor,
+		},
+		Item: ansi.StylePrimitive{
+			BlockPrefix:     "• ",
+			Color:           fgColor,
+			BackgroundColor: bgColor,
+		},
+		Enumeration: ansi.StylePrimitive{
+			BlockPrefix:     ". ",
+			Color:           fgColor,
+			BackgroundColor: bgColor,
+		},
+		Task: ansi.StyleTask{
+			StylePrimitive: ansi.StylePrimitive{
+				Color:           fgColor,
+				BackgroundColor: bgColor,
+			},
+			Ticked:   "[✓] ",
+			Unticked: "[ ] ",
+		},
+		Link: ansi.StylePrimitive{
+			Underline:       boolPtr(true),
+			Color:           fgColor,
+			BackgroundColor: bgColor,
+		},
+		LinkText: ansi.StylePrimitive{
+			Bold:            boolPtr(true),
+			Color:           fgColor,
+			BackgroundColor: bgColor,
+		},
+		Image: ansi.StylePrimitive{
+			Underline:       boolPtr(true),
+			Color:           fgColor,
+			BackgroundColor: bgColor,
+		},
+		ImageText: ansi.StylePrimitive{
+			Format:          "Image: {{.text}} →",
+			Color:           fgColor,
+			BackgroundColor: bgColor,
+		},
+		Code: ansi.StyleBlock{
+			StylePrimitive: ansi.StylePrimitive{
+				Prefix:          " ",
+				Suffix:          " ",
+				Color:           fgColor,
+				BackgroundColor: bgColor,
+			},
+		},
+		CodeBlock: ansi.StyleCodeBlock{
+			StyleBlock: ansi.StyleBlock{
+				StylePrimitive: ansi.StylePrimitive{
+					Color:           fgColor,
+					BackgroundColor: bgColor,
+				},
+				Margin: uintPtr(defaultMargin),
+			},
+		},
+		Table: ansi.StyleTable{
+			StyleBlock: ansi.StyleBlock{
+				StylePrimitive: ansi.StylePrimitive{
+					Color:           fgColor,
+					BackgroundColor: bgColor,
+				},
+			},
+		},
+		DefinitionDescription: ansi.StylePrimitive{
+			BlockPrefix:     "\n ",
+			Color:           fgColor,
+			BackgroundColor: bgColor,
+		},
+	}
+}

internal/tui/styles/theme.go 🔗

@@ -5,14 +5,14 @@ import (
 	"image/color"
 	"strings"
 
-	"github.com/charmbracelet/bubbles/v2/filepicker"
-	"github.com/charmbracelet/bubbles/v2/help"
-	"github.com/charmbracelet/bubbles/v2/textarea"
-	"github.com/charmbracelet/bubbles/v2/textinput"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/filepicker"
+	"charm.land/bubbles/v2/help"
+	"charm.land/bubbles/v2/textarea"
+	"charm.land/bubbles/v2/textinput"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/tui/exp/diffview"
 	"github.com/charmbracelet/glamour/v2/ansi"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/x/exp/charmtone"
 	"github.com/lucasb-eyer/go-colorful"
 	"github.com/rivo/uniseg"

internal/tui/tui.go 🔗

@@ -8,8 +8,10 @@ import (
 	"strings"
 	"time"
 
-	"github.com/charmbracelet/bubbles/v2/key"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/key"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
+	"github.com/charmbracelet/crush/internal/agent/tools/mcp"
 	"github.com/charmbracelet/crush/internal/app"
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/event"
@@ -32,7 +34,8 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/page/chat"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
-	"github.com/charmbracelet/lipgloss/v2"
+	"golang.org/x/text/cases"
+	"golang.org/x/text/language"
 )
 
 var lastMouseEvent time.Time
@@ -117,7 +120,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			a.sendProgressBar = slices.Contains(msg, "WT_SESSION")
 		}
 	case tea.TerminalVersionMsg:
-		termVersion := strings.ToLower(string(msg))
+		termVersion := strings.ToLower(msg.Name)
 		// Only enable progress bar for the following terminals.
 		if !a.sendProgressBar {
 			a.sendProgressBar = strings.Contains(termVersion, "ghostty")
@@ -138,6 +141,16 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		a.completions.Update(msg)
 		return a, a.handleWindowResize(msg.Width, msg.Height)
 
+	case pubsub.Event[mcp.Event]:
+		switch msg.Payload.Type {
+		case mcp.EventStateChanged:
+			return a, a.handleStateChanged(context.Background())
+		case mcp.EventPromptsListChanged:
+			return a, handleMCPPromptsEvent(context.Background(), msg.Payload.Name)
+		case mcp.EventToolsListChanged:
+			return a, handleMCPToolsEvent(context.Background(), msg.Payload.Name)
+		}
+
 	// Completions messages
 	case completions.OpenCompletionsMsg, completions.FilterCompletionsMsg,
 		completions.CloseCompletionsMsg, completions.RepositionCompletionsMsg:
@@ -156,15 +169,44 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		a.dialog = u.(dialogs.DialogCmp)
 		return a, tea.Batch(completionCmd, dialogCmd)
 	case commands.ShowArgumentsDialogMsg:
+		var args []commands.Argument
+		for _, arg := range msg.ArgNames {
+			args = append(args, commands.Argument{
+				Name:     arg,
+				Title:    cases.Title(language.English).String(arg),
+				Required: true,
+			})
+		}
 		return a, util.CmdHandler(
 			dialogs.OpenDialogMsg{
 				Model: commands.NewCommandArgumentsDialog(
 					msg.CommandID,
-					msg.Content,
-					msg.ArgNames,
+					msg.CommandID,
+					msg.CommandID,
+					msg.Description,
+					args,
+					msg.OnSubmit,
 				),
 			},
 		)
+	case commands.ShowMCPPromptArgumentsDialogMsg:
+		args := make([]commands.Argument, 0, len(msg.Prompt.Arguments))
+		for _, arg := range msg.Prompt.Arguments {
+			args = append(args, commands.Argument(*arg))
+		}
+		dialog := commands.NewCommandArgumentsDialog(
+			msg.Prompt.Name,
+			msg.Prompt.Title,
+			msg.Prompt.Name,
+			msg.Prompt.Description,
+			args,
+			msg.OnSubmit,
+		)
+		return a, util.CmdHandler(
+			dialogs.OpenDialogMsg{
+				Model: dialog,
+			},
+		)
 	// Page change messages
 	case page.PageChangeMsg:
 		return a, a.moveToPage(msg.ID)
@@ -587,6 +629,27 @@ func (a *appModel) View() tea.View {
 	return view
 }
 
+func (a *appModel) handleStateChanged(ctx context.Context) tea.Cmd {
+	return func() tea.Msg {
+		a.app.UpdateAgentModel(ctx)
+		return nil
+	}
+}
+
+func handleMCPPromptsEvent(ctx context.Context, name string) tea.Cmd {
+	return func() tea.Msg {
+		mcp.RefreshPrompts(ctx, name)
+		return nil
+	}
+}
+
+func handleMCPToolsEvent(ctx context.Context, name string) tea.Cmd {
+	return func() tea.Msg {
+		mcp.RefreshTools(ctx, name)
+		return nil
+	}
+}
+
 // New creates and initializes a new TUI application model.
 func New(app *app.App) *appModel {
 	chatPage := chat.New(app)

internal/tui/util/util.go 🔗

@@ -4,7 +4,7 @@ import (
 	"log/slog"
 	"time"
 
-	tea "github.com/charmbracelet/bubbletea/v2"
+	tea "charm.land/bubbletea/v2"
 )
 
 type Cursor interface {

internal/ui/common/interface.go 🔗

@@ -1,7 +1,7 @@
 package common
 
 import (
-	tea "github.com/charmbracelet/bubbletea/v2"
+	tea "charm.land/bubbletea/v2"
 )
 
 // Model represents a common interface for UI components.

internal/ui/dialog/dialog.go 🔗

@@ -1,10 +1,10 @@
 package dialog
 
 import (
-	"github.com/charmbracelet/bubbles/v2/key"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/key"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/ui/common"
-	"github.com/charmbracelet/lipgloss/v2"
 )
 
 // OverlayKeyMap defines key bindings for dialogs.

internal/ui/dialog/quit.go 🔗

@@ -1,10 +1,10 @@
 package dialog
 
 import (
-	"github.com/charmbracelet/bubbles/v2/key"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/key"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/ui/common"
-	"github.com/charmbracelet/lipgloss/v2"
 )
 
 // QuitDialogID is the identifier for the quit dialog.

internal/ui/logo/logo.go 🔗

@@ -6,9 +6,9 @@ import (
 	"image/color"
 	"strings"
 
+	"charm.land/lipgloss/v2"
 	"github.com/MakeNowJust/heredoc"
 	"github.com/charmbracelet/crush/internal/tui/styles"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/x/ansi"
 	"github.com/charmbracelet/x/exp/slice"
 )

internal/ui/model/chat.go 🔗

@@ -1,8 +1,8 @@
 package model
 
 import (
-	"github.com/charmbracelet/bubbles/v2/key"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/key"
+	tea "charm.land/bubbletea/v2"
 	"github.com/charmbracelet/crush/internal/app"
 	"github.com/charmbracelet/crush/internal/ui/common"
 )

internal/ui/model/editor.go 🔗

@@ -3,9 +3,9 @@ package model
 import (
 	"math/rand"
 
-	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/bubbles/v2/textarea"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/key"
+	"charm.land/bubbles/v2/textarea"
+	tea "charm.land/bubbletea/v2"
 	"github.com/charmbracelet/crush/internal/app"
 	"github.com/charmbracelet/crush/internal/ui/common"
 )

internal/ui/model/keys.go 🔗

@@ -1,6 +1,6 @@
 package model
 
-import "github.com/charmbracelet/bubbles/v2/key"
+import "charm.land/bubbles/v2/key"
 
 type KeyMap struct {
 	Quit     key.Binding

internal/ui/model/sidebar.go 🔗

@@ -1,12 +1,12 @@
 package model
 
 import (
-	tea "github.com/charmbracelet/bubbletea/v2"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/ui/common"
 	"github.com/charmbracelet/crush/internal/ui/logo"
 	"github.com/charmbracelet/crush/internal/ui/styles"
 	"github.com/charmbracelet/crush/internal/version"
-	"github.com/charmbracelet/lipgloss/v2"
 )
 
 // SidebarModel is the model for the sidebar UI component.

internal/ui/model/ui.go 🔗

@@ -6,13 +6,13 @@ import (
 	"slices"
 	"strings"
 
-	"github.com/charmbracelet/bubbles/v2/help"
-	"github.com/charmbracelet/bubbles/v2/key"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/help"
+	"charm.land/bubbles/v2/key"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/app"
 	"github.com/charmbracelet/crush/internal/ui/common"
 	"github.com/charmbracelet/crush/internal/ui/dialog"
-	"github.com/charmbracelet/lipgloss/v2"
 	uv "github.com/charmbracelet/ultraviolet"
 )
 
@@ -83,7 +83,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			m.sendProgressBar = slices.Contains(msg, "WT_SESSION")
 		}
 	case tea.TerminalVersionMsg:
-		termVersion := strings.ToLower(string(msg))
+		termVersion := strings.ToLower(msg.Name)
 		// Only enable progress bar for the following terminals.
 		if !m.sendProgressBar {
 			m.sendProgressBar = strings.Contains(termVersion, "ghostty")

internal/ui/styles/styles.go 🔗

@@ -3,14 +3,14 @@ package styles
 import (
 	"image/color"
 
-	"github.com/charmbracelet/bubbles/v2/filepicker"
-	"github.com/charmbracelet/bubbles/v2/help"
-	"github.com/charmbracelet/bubbles/v2/textarea"
-	"github.com/charmbracelet/bubbles/v2/textinput"
-	tea "github.com/charmbracelet/bubbletea/v2"
+	"charm.land/bubbles/v2/filepicker"
+	"charm.land/bubbles/v2/help"
+	"charm.land/bubbles/v2/textarea"
+	"charm.land/bubbles/v2/textinput"
+	tea "charm.land/bubbletea/v2"
+	"charm.land/lipgloss/v2"
 	"github.com/charmbracelet/crush/internal/tui/exp/diffview"
 	"github.com/charmbracelet/glamour/v2/ansi"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/x/exp/charmtone"
 )