mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
fix: ACP 模式下 extended thinking + tool_use 触发连续 user 消息导致 400 (CC-1215)
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
||||
AUTO_REJECT_MESSAGE,
|
||||
DONT_ASK_REJECT_MESSAGE,
|
||||
SYNTHETIC_MODEL,
|
||||
ensureToolResultPairing,
|
||||
} from '../messages'
|
||||
import type {
|
||||
Message,
|
||||
@@ -516,3 +517,96 @@ describe('normalizeMessagesForAPI', () => {
|
||||
expect(block._geminiThoughtSignature).toBe('sig-123')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureToolResultPairing', () => {
|
||||
test('does not produce consecutive user messages when orphaned tool_result is stripped after an existing user message (CC-1215)', () => {
|
||||
// Reproduce the scenario from the bug report:
|
||||
// Streaming yields assistant[thinking] and assistant[tool_use] separately.
|
||||
// normalizeMessagesForAPI merges them, but if the merge fails (e.g. intervening
|
||||
// user message breaks backward walk), ensureToolResultPairing sees duplicate
|
||||
// tool_use ID, strips it, leaving empty content in the next user message,
|
||||
// which becomes NO_CONTENT_MESSAGE. If the previous result entry is already
|
||||
// user, this must NOT create consecutive user messages.
|
||||
const toolUseId = 'toolu_test_dup_001'
|
||||
|
||||
const messages: (UserMessage | AssistantMessage)[] = [
|
||||
// Previous turn: user with tool_result
|
||||
createUserMessage({
|
||||
content: [
|
||||
{
|
||||
type: 'tool_result',
|
||||
tool_use_id: toolUseId,
|
||||
content: 'previous result',
|
||||
},
|
||||
],
|
||||
}),
|
||||
// Current turn: assistant with thinking only (tool_use was deduped away)
|
||||
makeAssistantMsg([{ type: 'thinking', thinking: 'let me think...' }]),
|
||||
// Current turn: assistant with tool_use (second streaming yield, same ID)
|
||||
makeAssistantMsg([
|
||||
{
|
||||
type: 'tool_use',
|
||||
id: toolUseId,
|
||||
name: 'Bash',
|
||||
input: { command: 'pwd' },
|
||||
},
|
||||
]),
|
||||
// Tool result for the tool_use
|
||||
createUserMessage({
|
||||
content: [
|
||||
{
|
||||
type: 'tool_result',
|
||||
tool_use_id: toolUseId,
|
||||
content: '/home/user',
|
||||
},
|
||||
],
|
||||
}),
|
||||
]
|
||||
|
||||
const result = ensureToolResultPairing(messages)
|
||||
|
||||
// Verify no consecutive user messages
|
||||
for (let i = 1; i < result.length; i++) {
|
||||
if (result[i - 1]!.type === 'user') {
|
||||
expect(result[i]!.type).not.toBe('user')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('inserts NO_CONTENT_MESSAGE when previous result entry is assistant', () => {
|
||||
// When the orphan strip empties a user message and the previous entry is
|
||||
// assistant, the placeholder should still be inserted to maintain alternation.
|
||||
const toolUseId = 'toolu_test_orphan_001'
|
||||
|
||||
const messages: (UserMessage | AssistantMessage)[] = [
|
||||
makeAssistantMsg([{ type: 'text', text: 'hello' }]),
|
||||
// This assistant has a tool_use with an ID that won't match any result
|
||||
makeAssistantMsg([
|
||||
{
|
||||
type: 'tool_use',
|
||||
id: toolUseId,
|
||||
name: 'Bash',
|
||||
input: { command: 'ls' },
|
||||
},
|
||||
]),
|
||||
// User message with ONLY a tool_result for a non-existent tool_use
|
||||
// After orphan stripping, content becomes empty
|
||||
createUserMessage({
|
||||
content: [
|
||||
{
|
||||
type: 'tool_result',
|
||||
tool_use_id: 'nonexistent_id',
|
||||
content: 'orphan',
|
||||
},
|
||||
],
|
||||
}),
|
||||
]
|
||||
|
||||
const result = ensureToolResultPairing(messages)
|
||||
|
||||
// Should have assistant, [possibly modified assistant], user placeholder
|
||||
// The key assertion: last message should be a user placeholder
|
||||
const lastMsg = result[result.length - 1]!
|
||||
expect(lastMsg.type).toBe('user')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5829,11 +5829,15 @@ export function ensureToolResultPairing(
|
||||
)
|
||||
} else {
|
||||
// Content is empty after stripping orphaned tool_results. We still
|
||||
// need a user message here to maintain role alternation — otherwise
|
||||
// the assistant placeholder we just pushed would be immediately
|
||||
// followed by the NEXT assistant message, which the API rejects with
|
||||
// a role-alternation 400 (not the duplicate-id 400 we handle).
|
||||
// need a user message here to maintain role alternation — unless the
|
||||
// previous result entry is already a user message, in which case
|
||||
// inserting another user placeholder creates consecutive-user messages
|
||||
// that Anthropic rejects with a misleading "tool_use without
|
||||
// tool_result" 400 (CC-1215).
|
||||
i++
|
||||
if (result.at(-1)?.type === 'user') {
|
||||
continue
|
||||
}
|
||||
result.push(
|
||||
createUserMessage({
|
||||
content: NO_CONTENT_MESSAGE,
|
||||
|
||||
Reference in New Issue
Block a user