fix(core): strip orphaned user entries before retry to prevent API errors

Replace isContinuation boolean with SendMessageType enum for clearer
message type semantics. Add stripOrphanedUserEntriesFromHistory() to
clean up orphaned user entries in chat history before retry operations,
preventing 'messages with role tool must be a response to preceding
message with tool_calls' API errors.

Fixes #2360

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-03-14 12:51:34 +08:00
parent f1ee4638b7
commit 88685e55f6
8 changed files with 242 additions and 67 deletions

View file

@ -28,6 +28,7 @@ import {
ApprovalMode,
AuthType,
GeminiEventType as ServerGeminiEventType,
SendMessageType,
ToolErrorType,
ToolConfirmationOutcome,
} from '@qwen-code/qwen-code-core';
@ -482,7 +483,7 @@ describe('useGeminiStream', () => {
expectedMergedResponse,
expect.any(AbortSignal),
'prompt-id-2',
{ isContinuation: true },
{ type: SendMessageType.ToolResult },
);
});
@ -806,7 +807,7 @@ describe('useGeminiStream', () => {
toolCallResponseParts,
expect.any(AbortSignal),
'prompt-id-4',
{ isContinuation: true },
{ type: SendMessageType.ToolResult },
);
});
@ -1122,7 +1123,7 @@ describe('useGeminiStream', () => {
'This is the actual prompt from the command file.',
expect.any(AbortSignal),
expect.any(String),
undefined,
{ type: SendMessageType.UserQuery },
);
expect(mockScheduleToolCalls).not.toHaveBeenCalled();
@ -1149,7 +1150,7 @@ describe('useGeminiStream', () => {
'',
expect.any(AbortSignal),
expect.any(String),
undefined,
{ type: SendMessageType.UserQuery },
);
});
});
@ -1168,7 +1169,7 @@ describe('useGeminiStream', () => {
'// This is a line comment',
expect.any(AbortSignal),
expect.any(String),
undefined,
{ type: SendMessageType.UserQuery },
);
});
});
@ -1187,7 +1188,7 @@ describe('useGeminiStream', () => {
'/* This is a block comment */',
expect.any(AbortSignal),
expect.any(String),
undefined,
{ type: SendMessageType.UserQuery },
);
});
});
@ -2091,7 +2092,7 @@ describe('useGeminiStream', () => {
processedQueryParts, // Argument 1: The parts array directly
expect.any(AbortSignal), // Argument 2: An AbortSignal
expect.any(String), // Argument 3: The prompt_id string
undefined, // Argument 4: Options (undefined for normal prompts)
{ type: SendMessageType.UserQuery }, // Argument 4: The options
);
});
@ -2776,7 +2777,7 @@ describe('useGeminiStream', () => {
'First query',
expect.any(AbortSignal),
expect.any(String),
undefined,
{ type: SendMessageType.UserQuery },
);
// Verify only the first query was added to history
@ -2828,14 +2829,14 @@ describe('useGeminiStream', () => {
'First query',
expect.any(AbortSignal),
expect.any(String),
undefined,
{ type: SendMessageType.UserQuery },
);
expect(mockSendMessageStream).toHaveBeenNthCalledWith(
2,
'Second query',
expect.any(AbortSignal),
expect.any(String),
undefined,
{ type: SendMessageType.UserQuery },
);
});
@ -2858,7 +2859,7 @@ describe('useGeminiStream', () => {
'Second query',
expect.any(AbortSignal),
expect.any(String),
undefined,
{ type: SendMessageType.UserQuery },
);
});
});