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,
|
AUTO_REJECT_MESSAGE,
|
||||||
DONT_ASK_REJECT_MESSAGE,
|
DONT_ASK_REJECT_MESSAGE,
|
||||||
SYNTHETIC_MODEL,
|
SYNTHETIC_MODEL,
|
||||||
|
ensureToolResultPairing,
|
||||||
} from '../messages'
|
} from '../messages'
|
||||||
import type {
|
import type {
|
||||||
Message,
|
Message,
|
||||||
@@ -516,3 +517,96 @@ describe('normalizeMessagesForAPI', () => {
|
|||||||
expect(block._geminiThoughtSignature).toBe('sig-123')
|
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 {
|
} else {
|
||||||
// Content is empty after stripping orphaned tool_results. We still
|
// Content is empty after stripping orphaned tool_results. We still
|
||||||
// need a user message here to maintain role alternation — otherwise
|
// need a user message here to maintain role alternation — unless the
|
||||||
// the assistant placeholder we just pushed would be immediately
|
// previous result entry is already a user message, in which case
|
||||||
// followed by the NEXT assistant message, which the API rejects with
|
// inserting another user placeholder creates consecutive-user messages
|
||||||
// a role-alternation 400 (not the duplicate-id 400 we handle).
|
// that Anthropic rejects with a misleading "tool_use without
|
||||||
|
// tool_result" 400 (CC-1215).
|
||||||
i++
|
i++
|
||||||
|
if (result.at(-1)?.type === 'user') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
result.push(
|
result.push(
|
||||||
createUserMessage({
|
createUserMessage({
|
||||||
content: NO_CONTENT_MESSAGE,
|
content: NO_CONTENT_MESSAGE,
|
||||||
|
|||||||
Reference in New Issue
Block a user