fix(core): skip merged function-response turns when finding the active loop (#28565)

This commit is contained in:
Adam Weidman
2026-07-28 16:16:53 -04:00
committed by GitHub
parent fccc043bd4
commit d29268d360
2 changed files with 41 additions and 2 deletions
+31
View File
@@ -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[] = [
+10 -2
View File
@@ -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;
}