diff --git a/packages/core/src/core/geminiChat.test.ts b/packages/core/src/core/geminiChat.test.ts index 83d97e59d9..1524ef3b61 100644 --- a/packages/core/src/core/geminiChat.test.ts +++ b/packages/core/src/core/geminiChat.test.ts @@ -2447,6 +2447,37 @@ describe('GeminiChat', () => { expect(newContents[5]?.parts?.[1]).not.toHaveProperty('thoughtSignature'); }); + it('should skip a user turn that has text alongside a functionResponse when locating the active loop', () => { + const chat = new GeminiChat(mockConfig, '', [], []); + // `coalesceConsecutiveRoles` can merge a function response turn with the + // prompt that follows it, producing a user turn holding both. + const history: Content[] = [ + { role: 'user', parts: [{ text: 'First prompt' }] }, + { + role: 'model', + parts: [ + { text: 'Working on it' }, + { functionCall: { name: 'some_tool', args: {} } }, + ], + }, + { + role: 'user', + parts: [ + { functionResponse: { name: 'some_tool', response: {} } }, + { text: 'Second prompt' }, + ], + }, + ]; + + const newContents = chat.ensureActiveLoopHasThoughtSignatures(history); + + // The merged turn must not be taken as the loop start, otherwise the + // model turn before it is left unsigned while the API still validates it. + expect(newContents[1]?.parts?.[1]?.thoughtSignature).toBe( + SYNTHETIC_THOUGHT_SIGNATURE, + ); + }); + it('should not modify contents if there is no user text message', () => { const chat = new GeminiChat(mockConfig, '', [], []); const history: Content[] = [ diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index e573a69060..56480c70f0 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -1044,11 +1044,19 @@ export class GeminiChat { requestContents: readonly Content[], ): readonly Content[] { // First, find the start of the active loop by finding the last user turn - // with a text message, i.e. that is not a function response. + // with a text message, i.e. that is not a function response. Testing for + // text alone is not enough: `coalesceConsecutiveRoles` can merge a function + // response turn with the prompt that follows it, and starting the loop at + // such a turn starts it later than the API starts the turn, leaving earlier + // function calls unsigned but still validated. let activeLoopStartIndex = -1; for (let i = requestContents.length - 1; i >= 0; i--) { const content = requestContents[i]; - if (content.role === 'user' && content.parts?.some((part) => part.text)) { + if ( + content.role === 'user' && + content.parts?.some((part) => part.text) && + !content.parts?.some((part) => part.functionResponse) + ) { activeLoopStartIndex = i; break; }