fix(chatd): keep provider-executed tool results in assistant content (#22991)

## Problem

Anthropic's API returns a 400 error when `web_search` tool results are
missing:

```
web_search tool use with id srvtoolu_... was found without a corresponding web_search_tool_result block
```

**Root cause:** `persistStep` in `chatd.go` splits ALL
`ToolResultContent` blocks into separate tool-role DB rows.
Provider-executed (PE) tool results like `web_search` must stay in the
assistant message — Anthropic expects `server_tool_use` and
`web_search_tool_result` in the same turn.

The previous fix (#22976) added repair passes to drop PE results during
reconstruction, which fixed cross-step orphans but broke the normal case
(PE result correctly in the same step).

## Fix

Three changes that address the root cause:

1. **`persistStep` (chatd.go):** Check `ProviderExecuted` before
splitting `ToolResultContent` into tool rows. PE results stay in
`assistantBlocks` and are stored in the assistant content column.

2. **`ToMessageParts` (chatprompt.go):** Propagate the
`ProviderExecuted` field to `ToolResultPart` so the fantasy Anthropic
provider can identify PE results and reconstruct the
`web_search_tool_result` block.

3. **Keep existing repair passes** for backward compatibility with
legacy DB data where PE results were incorrectly persisted as separate
tool messages.

## Tests

- `TestProviderExecutedResultInAssistantContent` — PE result stored
inline in assistant content round-trips correctly with
`ProviderExecuted` preserved.
- `TestProviderExecutedResult_LegacyToolRow` — legacy PE results in
tool-role rows are still dropped correctly.
- All existing tests pass (including the 3 PE tests from #22976).
This commit is contained in:
Kyle Carberry
2026-03-12 09:49:53 -04:00
committed by GitHub
parent 2b70122e4a
commit c3923f2ccd
3 changed files with 144 additions and 11 deletions
+12 -5
View File
@@ -2298,17 +2298,24 @@ func (p *Server) runChat(
// Split the step content into assistant blocks and tool
// result blocks so they can be stored as separate messages
// with the appropriate roles.
// with the appropriate roles. Provider-executed tool results
// (e.g. web_search) stay in the assistant content because
// the LLM provider expects them inline in the assistant
// turn, not as separate tool messages.
var assistantBlocks []fantasy.Content
var toolResults []fantasy.ToolResultContent
for _, block := range step.Content {
if tr, ok := fantasy.AsContentType[fantasy.ToolResultContent](block); ok {
toolResults = append(toolResults, tr)
continue
if !tr.ProviderExecuted {
toolResults = append(toolResults, tr)
continue
}
}
if trPtr, ok := fantasy.AsContentType[*fantasy.ToolResultContent](block); ok && trPtr != nil {
toolResults = append(toolResults, *trPtr)
continue
if !trPtr.ProviderExecuted {
toolResults = append(toolResults, *trPtr)
continue
}
}
assistantBlocks = append(assistantBlocks, block)
}