mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
fix(core): flush transcript for pure tool-call responses to ensure BeforeTool hooks see complete state (#20419)
Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
This commit is contained in:
@@ -1032,6 +1032,59 @@ describe('GeminiChat', () => {
|
|||||||
LlmRole.MAIN,
|
LlmRole.MAIN,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should flush transcript before tool dispatch for pure tool call with no text or thoughts', async () => {
|
||||||
|
const pureToolCallStream = (async function* () {
|
||||||
|
yield {
|
||||||
|
candidates: [
|
||||||
|
{
|
||||||
|
content: {
|
||||||
|
role: 'model',
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
functionCall: {
|
||||||
|
name: 'read_file',
|
||||||
|
args: { path: 'test.py' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as GenerateContentResponse;
|
||||||
|
})();
|
||||||
|
|
||||||
|
vi.mocked(mockContentGenerator.generateContentStream).mockResolvedValue(
|
||||||
|
pureToolCallStream,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { default: fs } = await import('node:fs');
|
||||||
|
const writeFileSync = vi.mocked(fs.writeFileSync);
|
||||||
|
const writeCountBefore = writeFileSync.mock.calls.length;
|
||||||
|
|
||||||
|
const stream = await chat.sendMessageStream(
|
||||||
|
{ model: 'test-model' },
|
||||||
|
'analyze test.py',
|
||||||
|
'prompt-id-pure-tool-flush',
|
||||||
|
new AbortController().signal,
|
||||||
|
LlmRole.MAIN,
|
||||||
|
);
|
||||||
|
for await (const _ of stream) {
|
||||||
|
// consume
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWrites = writeFileSync.mock.calls.slice(writeCountBefore);
|
||||||
|
expect(newWrites.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const lastWriteData = JSON.parse(
|
||||||
|
newWrites[newWrites.length - 1][1] as string,
|
||||||
|
) as { messages: Array<{ type: string }> };
|
||||||
|
|
||||||
|
const geminiMessages = lastWriteData.messages.filter(
|
||||||
|
(m) => m.type === 'gemini',
|
||||||
|
);
|
||||||
|
expect(geminiMessages.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('addHistory', () => {
|
describe('addHistory', () => {
|
||||||
|
|||||||
@@ -818,6 +818,7 @@ export class GeminiChat {
|
|||||||
const modelResponseParts: Part[] = [];
|
const modelResponseParts: Part[] = [];
|
||||||
|
|
||||||
let hasToolCall = false;
|
let hasToolCall = false;
|
||||||
|
let hasThoughts = false;
|
||||||
let finishReason: FinishReason | undefined;
|
let finishReason: FinishReason | undefined;
|
||||||
|
|
||||||
for await (const chunk of streamResponse) {
|
for await (const chunk of streamResponse) {
|
||||||
@@ -834,6 +835,7 @@ export class GeminiChat {
|
|||||||
if (content?.parts) {
|
if (content?.parts) {
|
||||||
if (content.parts.some((part) => part.thought)) {
|
if (content.parts.some((part) => part.thought)) {
|
||||||
// Record thoughts
|
// Record thoughts
|
||||||
|
hasThoughts = true;
|
||||||
this.recordThoughtFromContent(content);
|
this.recordThoughtFromContent(content);
|
||||||
}
|
}
|
||||||
if (content.parts.some((part) => part.functionCall)) {
|
if (content.parts.some((part) => part.functionCall)) {
|
||||||
@@ -901,8 +903,10 @@ export class GeminiChat {
|
|||||||
.join('')
|
.join('')
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
// Record model response text from the collected parts
|
// Record model response text from the collected parts.
|
||||||
if (responseText) {
|
// Also flush when there are thoughts or a tool call (even with no text)
|
||||||
|
// so that BeforeTool hooks always see the latest transcript state.
|
||||||
|
if (responseText || hasThoughts || hasToolCall) {
|
||||||
this.chatRecordingService.recordMessage({
|
this.chatRecordingService.recordMessage({
|
||||||
model,
|
model,
|
||||||
type: 'gemini',
|
type: 'gemini',
|
||||||
|
|||||||
Reference in New Issue
Block a user