Skip to content

Commit e3c2819

Browse files
committed
fix(#2053): add pendingAssistantToolUse guard to beta message converter
The non-beta convertMessages has a pendingAssistantToolUse flag that only includes tool_result user messages when they immediately follow an assistant message with tool_use blocks. Orphan tool results from corrupted session history are silently dropped. The beta convertBetaMessages had no such guard — every tool role message was unconditionally converted to a tool_result block. When the session history contained orphan tool results (e.g. from sub-agent messages that leaked into the parent session), they passed straight through to the Anthropic API, causing: "unexpected tool_use_id found in tool_result blocks" Add the same pendingAssistantToolUse tracking to convertBetaMessages to match the non-beta converter behavior. Assisted-By: docker-agent
1 parent b3f900d commit e3c2819

1 file changed

Lines changed: 25 additions & 24 deletions

File tree

pkg/model/provider/anthropic/beta_converter.go

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
// blocks from the same assistant message MUST be grouped into a single user message.
2424
func (c *Client) convertBetaMessages(ctx context.Context, messages []chat.Message) ([]anthropic.BetaMessageParam, error) {
2525
var betaMessages []anthropic.BetaMessageParam
26+
pendingAssistantToolUse := false
2627

2728
for i := 0; i < len(messages); i++ {
2829
msg := &messages[i]
@@ -75,20 +76,19 @@ func (c *Client) convertBetaMessages(ctx context.Context, messages []chat.Messag
7576
}
7677

7778
// Add tool calls
78-
if len(msg.ToolCalls) > 0 {
79-
for _, toolCall := range msg.ToolCalls {
80-
var inpts map[string]any
81-
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &inpts); err != nil {
82-
inpts = map[string]any{}
83-
}
84-
contentBlocks = append(contentBlocks, anthropic.BetaContentBlockParamUnion{
85-
OfToolUse: &anthropic.BetaToolUseBlockParam{
86-
ID: toolCall.ID,
87-
Input: inpts,
88-
Name: toolCall.Function.Name,
89-
},
90-
})
79+
hasToolCalls := len(msg.ToolCalls) > 0
80+
for _, toolCall := range msg.ToolCalls {
81+
var inpts map[string]any
82+
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &inpts); err != nil {
83+
inpts = map[string]any{}
9184
}
85+
contentBlocks = append(contentBlocks, anthropic.BetaContentBlockParamUnion{
86+
OfToolUse: &anthropic.BetaToolUseBlockParam{
87+
ID: toolCall.ID,
88+
Input: inpts,
89+
Name: toolCall.Function.Name,
90+
},
91+
})
9292
}
9393

9494
if len(contentBlocks) > 0 {
@@ -97,28 +97,29 @@ func (c *Client) convertBetaMessages(ctx context.Context, messages []chat.Messag
9797
Content: contentBlocks,
9898
})
9999
}
100+
pendingAssistantToolUse = hasToolCalls
100101
continue
101102
}
102103
if msg.Role == chat.MessageRoleTool {
103104
// Collect consecutive tool messages and merge them into a single user message
104105
// This is required by Anthropic API: all tool_result blocks for tool_use blocks
105106
// from the same assistant message must be in the same user message
106-
toolResultBlocks := []anthropic.BetaContentBlockParamUnion{
107-
convertBetaToolResultBlock(msg),
108-
}
109-
110-
// Look ahead for consecutive tool messages and merge them
111-
j := i + 1
107+
var toolResultBlocks []anthropic.BetaContentBlockParamUnion
108+
j := i
112109
for j < len(messages) && messages[j].Role == chat.MessageRoleTool {
113110
toolResultBlocks = append(toolResultBlocks, convertBetaToolResultBlock(&messages[j]))
114111
j++
115112
}
116113

117-
// Add the merged user message with all tool results
118-
betaMessages = append(betaMessages, anthropic.BetaMessageParam{
119-
Role: anthropic.BetaMessageParamRoleUser,
120-
Content: toolResultBlocks,
121-
})
114+
// Only include tool results if they follow an assistant message with tool_use.
115+
// Orphan tool_result blocks (e.g. from corrupted session history) are dropped.
116+
if pendingAssistantToolUse && len(toolResultBlocks) > 0 {
117+
betaMessages = append(betaMessages, anthropic.BetaMessageParam{
118+
Role: anthropic.BetaMessageParamRoleUser,
119+
Content: toolResultBlocks,
120+
})
121+
}
122+
pendingAssistantToolUse = false
122123

123124
// Skip the messages we've already processed
124125
i = j - 1

0 commit comments

Comments
 (0)