google-gemini/gemini-cli

Turn drops non-thought parts when a chunk's first part is a thought

Summary

  • Context: The Turn class in packages/core/src/core/turn.ts processes streaming responses from the Gemini API, yielding events for thoughts, content, function calls, and citations.

  • Bug: When a response chunk’s first part contains a thought, all other parts in that chunk (text content, function calls, citations) are silently discarded.

  • Actual vs. expected: The code skips processing remaining parts after encountering a thought, whereas it should process all parts while filtering out the thought.

  • Impact: Users lose response content, function calls fail to execute, citations are not displayed, and finish reasons are not processed when they arrive in the same chunk as a thought.

Code with bug

const thoughtPart = resp.candidates?.[0]?.content?.parts?.[0];
if (thoughtPart?.thought) {
  const thought = parseThought(thoughtPart.text ?? '');
  yield {
    type: GeminiEventType.Thought,
    value: thought,
    traceId,
  };
  continue;  // <-- BUG 🔴 Skips processing of remaining parts
}

Example

Given a Gemini response chunk that mixes a thought with text and other data:

{
  candidates: [{
    content: {
      parts: [
        { text: '**Planning** the solution', thought: 'planning' },
        { text: 'I will help you with that.' },
      ]
    },
    citationMetadata: { citations: [{ uri: '<https://example.com>', title: 'Source' }] },
    finishReason: 'STOP'
  }],
  functionCalls: [
    { id: 'fc1', name: 'ReadFile', args: { path: 'file.txt' } }
  ]
}

Expected behavior:

  • Yield Thought event

  • Yield Content event with “I will help you with that.”

  • Yield ToolCallRequest for ReadFile

  • Collect the citation and later emit it on finish

  • Yield Finished event with reason STOP

Actual behavior (bug):

  • Yields Thought event, then continues to next chunk

  • Content, function call, citations, and finish reason from this chunk are not processed

Recommended fix

  1. Remove the continue so other parts are processed after yielding the Thought.

  2. Filter out thought parts when extracting text (or update the shared text-extraction util to do so):

// Keep processing after yielding the thought
const thoughtPart = resp.candidates?.[0]?.content?.parts?.[0];
if (thoughtPart?.thought) {
  const thought = parseThought(thoughtPart.text ?? '');
  yield { type: GeminiEventType.Thought, value: thought, traceId };
  // Don't continue - keep processing other parts  // <-- FIX 🟢 Remove the continue statement
}

// Extract text from non-thought parts only  // <-- FIX 🟢 Filter thought parts
const parts = resp.candidates?.[0]?.content?.parts ?? [];
const nonThoughtText = parts
  .filter((part) => !part.thought && part.text)
  .map((part) => part.text)
  .join('');

if (nonThoughtText) {
  yield { type: GeminiEventType.Content, value: nonThoughtText, traceId };
}

Alternatively, update getResponseText() in packages/core/src/utils/partUtils.ts to filter thought parts:

return candidate.content.parts
  .filter((part) => part.text && !part.thought)
  .map((part) => part.text)
  .join('');