diff --git a/packages/core/src/core/turn.test.ts b/packages/core/src/core/turn.test.ts index be94945476..615cb47541 100644 --- a/packages/core/src/core/turn.test.ts +++ b/packages/core/src/core/turn.test.ts @@ -134,6 +134,44 @@ describe('Turn', () => { expect(turn.getDebugResponses().length).toBe(2); }); + it('should not duplicate text in getResponseText when both chunks and consolidated response are present', async () => { + const mockResponseStream = (async function* () { + // Chunk 1 + yield { + type: StreamEventType.CHUNK, + value: { + candidates: [{ content: { parts: [{ text: 'Hello' }] } }], + } as GenerateContentResponse, + }; + // Chunk 2 + yield { + type: StreamEventType.CHUNK, + value: { + candidates: [{ content: { parts: [{ text: ' world' }] } }], + } as GenerateContentResponse, + }; + // Final consolidated response (as yielded by GeminiChat) + yield { + type: StreamEventType.CHUNK, + value: { + candidates: [{ + content: { parts: [{ text: 'Hello world' }] }, + finishReason: 'STOP' + }], + } as GenerateContentResponse, + }; + })(); + mockSendMessageStream.mockResolvedValue(mockResponseStream); + + for await (const _ of turn.run({ model: 'm' }, [{ text: 'req' }], new AbortController().signal)) { + // consume stream + } + + const text = turn.getResponseText(); + // CURRENT BUGGY BEHAVIOR: "Hello world Hello world" + expect(text).toBe('Hello world'); + }); + it('should yield tool_call_request events for function calls', async () => { const mockResponseStream = (async function* () { yield { diff --git a/packages/core/src/core/turn.ts b/packages/core/src/core/turn.ts index cc5335981a..f4703ecc53 100644 --- a/packages/core/src/core/turn.ts +++ b/packages/core/src/core/turn.ts @@ -472,10 +472,21 @@ export class Turn { */ getResponseText(): string { if (this.cachedResponseText === undefined) { - this.cachedResponseText = this.debugResponses - .map((response) => getResponseText(response)) - .filter((text): text is string => text !== null) - .join(' '); + let result = ''; + for (const response of this.debugResponses) { + const text = getResponseText(response); + if (text) { + // Heuristic to handle both delta and cumulative responses: + // If the new text starts with our current result, it's likely a cumulative + // update (or a redundant final consolidated response). + if (result && text.startsWith(result)) { + result = text; + } else { + result += text; + } + } + } + this.cachedResponseText = result; } return this.cachedResponseText; }