Skip to content

fix: skip empty text finalization chunk after function call#270

Open
RobinClowers wants to merge 1 commit into
google:mainfrom
RobinClowers:fix/suppress-empty-finalization-after-function-call
Open

fix: skip empty text finalization chunk after function call#270
RobinClowers wants to merge 1 commit into
google:mainfrom
RobinClowers:fix/suppress-empty-finalization-after-function-call

Conversation

@RobinClowers
Copy link
Copy Markdown
Contributor

Link to Issue or Description of Change

Problem:
Gemini thinking models send an empty text chunk with finishReason STOP after a function call to signal completion. The streaming loop in generateContentAsync was yielding this chunk unconditionally, causing isFinalResponse() to treat it as the agent's final response and preventing the follow-up model call that generates the actual text.

Solution:
Track whether a function call has been seen in the current streaming turn. When we encounter a chunk that has only empty text parts and finishReason STOP after a function call, skip it instead of yielding.

Testing Plan

Please describe the tests that you ran to verify your changes. This is required
for all PRs that are not small documentation or typo fixes.

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Please include a summary of passed npm test results.

Manual End-to-End (E2E) Tests:

The bug triggers when a thinking model makes a function call during streaming — Gemini sends the function call chunk, then an empty text chunk with finishReason: STOP. Without the fix, the agent treats that
empty chunk as the final response and never makes the second model call to generate actual text.

Setup: An agent with a tool and a thinking model:

const getWeather = new FunctionTool({
  name: 'get_weather',
  description: 'Get current weather for a city',
  inputSchema: z.object({ city: z.string() }),
  execute: async ({ city }) => ({ city, temp: '72F', condition: 'sunny' }),
});

const agent = new LlmAgent({
  name: 'test_agent',
  model: 'gemini-3-flash-preview',
  instruction: 'You help with weather questions. Always use the get_weather tool.',
  tools: [getWeather],
});

A simple prompt like "What's the weather in Seattle?" is enough — you just need one tool call.

What to look for:

  • Without the fix: The run ends immediately after the tool response. You get events for the function call and function response, but no final text from the model. The agent appears to silently stop mid-turn. No
    error is thrown — it just produces no answer.
  • With the fix: The run completes with a final text event where the model summarizes the tool result (e.g., "The weather in Seattle is 72F and sunny").

Key conditions:

  • Must use streaming (the default)
  • Must use a thinking model — non-thinking models don't send the empty finalization chunk
  • Only needs a single tool call to trigger, so it's more reliably reproducible than the thoughtSignature bug
  • If the model decides not to use the tool (unlikely with the instruction), there's no function call and the bug doesn't apply — just retry

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

@google-cla
Copy link
Copy Markdown

google-cla Bot commented Apr 14, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@RobinClowers RobinClowers force-pushed the fix/suppress-empty-finalization-after-function-call branch 2 times, most recently from 6aafbbe to 6a4f9bc Compare April 16, 2026 16:19
@ScottMansfield
Copy link
Copy Markdown
Member

I created a local e2e test that uses your exact function and agent. It seems like it's working fine, I see the functionResponse with STOP and a final event with model output. I pushed a branch called weather-tool-test with this test.

Is there something else that you know would be different from that setup?

@RobinClowers
Copy link
Copy Markdown
Contributor Author

@ScottMansfield thanks for creating that test, and sorry for the late response. I pushed a commit to my fork that causes the test to fail https://github.com/RobinClowers/adk-js/tree/weather-tool-test. Two key changes: multiple turns and enabling SSE streaming. I'll also resolve the merge conflicts here.

Gemini thinking models send an empty text chunk with finishReason STOP
after a function call to signal completion. The streaming loop in
generateContentAsync was yielding this chunk unconditionally, causing
isFinalResponse() to treat it as the agent's final response and
preventing the follow-up model call that generates the actual text.

Track whether a function call has been seen in the current streaming
turn. When we encounter a chunk that has only empty text parts and
finishReason STOP after a function call, skip it instead of yielding.
@RobinClowers RobinClowers force-pushed the fix/suppress-empty-finalization-after-function-call branch from 6a4f9bc to 2a6d3c6 Compare April 21, 2026 22:44
@RobinClowers
Copy link
Copy Markdown
Contributor Author

@ScottMansfield checking in on this, anything else you need from me?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants