[GH-ISSUE #314] Gemini models fail with 400: function response parts mismatch from FilesystemMiddleware #247

Closed
opened 2026-06-05 17:21:15 -04:00 by yindo · 2 comments
Owner

Originally created by @Lovakovic on GitHub (Mar 17, 2026).
Original GitHub issue: https://github.com/langchain-ai/deepagentsjs/issues/314

Bug Description

Using createDeepAgent() with a Google Gemini model (gemini-3.1-pro-preview via @langchain/google ChatGoogle) consistently fails on the very first model invocation with:

400 INVALID_ARGUMENT: "Please ensure that the number of function response parts 
is equal to the number of function call parts of the function call turn."

Environment

  • deepagents: 1.8.3
  • @langchain/google: 2.3.0
  • Node.js: 25.3.0
  • Model: gemini-3.1-pro-preview via Vertex AI (platformType: 'gcp', apiVersion: 'v1beta1')
  • Streaming: Yes (via agent.stream())

Reproduction

import { createDeepAgent } from 'deepagents';
import { ChatGoogle } from '@langchain/google/node';
import { HumanMessage } from '@langchain/core/messages';

const model = new ChatGoogle({
  model: 'gemini-3.1-pro-preview',
  temperature: 0,
  apiVersion: 'v1beta1',
  location: 'global',
  platformType: 'gcp',
});

const agent = createDeepAgent({
  model,
  backend: mySandboxBackend, // any BaseSandbox implementation
  name: 'test-agent',
  systemPrompt: 'You are a helpful agent.',
});

const stream = await agent.stream(
  { messages: [new HumanMessage('List files in /workspace')] },
  { configurable: { thread_id: 'test-1' }, recursionLimit: 100, streamMode: ['updates', 'messages'], subgraphs: true }
);

for await (const event of stream) { /* fails immediately */ }

Root Cause Analysis

The Gemini API strictly requires that every function call in a conversation turn has exactly one matching function response. Other model providers (OpenAI, Anthropic) are more lenient about this.

The FilesystemMiddleware (and possibly MemoryMiddleware/SkillsMiddleware) appears to construct message history during initialization that includes tool call/response pairs with mismatched counts. When this history is sent to Gemini's streamGenerateContent endpoint, the API rejects it.

I verified this is not a checkpoint issue (clean checkpoints, same error) and not caused by memory/skills parameters (removed them, same error — FilesystemMiddleware always runs when backend is provided).

Note: createDeepAgent() works fine with Gemini for basic calls without a backend — the issue only manifests when FilesystemMiddleware is active.

Related Issues

Workaround Attempts

  • Removing memory and skills params → still fails (FilesystemMiddleware is implicit)
  • Clearing checkpoint state → still fails (not a stale state issue)
  • Non-streaming mode → not easily configurable through createDeepAgent

Expected Behavior

createDeepAgent() should work with Gemini models, ensuring that the message history sent to the model always maintains 1:1 tool call/response parity.

Originally created by @Lovakovic on GitHub (Mar 17, 2026). Original GitHub issue: https://github.com/langchain-ai/deepagentsjs/issues/314 ## Bug Description Using `createDeepAgent()` with a Google Gemini model (`gemini-3.1-pro-preview` via `@langchain/google` `ChatGoogle`) consistently fails on the very first model invocation with: ``` 400 INVALID_ARGUMENT: "Please ensure that the number of function response parts is equal to the number of function call parts of the function call turn." ``` ## Environment - **deepagents**: 1.8.3 - **@langchain/google**: 2.3.0 - **Node.js**: 25.3.0 - **Model**: `gemini-3.1-pro-preview` via Vertex AI (`platformType: 'gcp'`, `apiVersion: 'v1beta1'`) - **Streaming**: Yes (via `agent.stream()`) ## Reproduction ```typescript import { createDeepAgent } from 'deepagents'; import { ChatGoogle } from '@langchain/google/node'; import { HumanMessage } from '@langchain/core/messages'; const model = new ChatGoogle({ model: 'gemini-3.1-pro-preview', temperature: 0, apiVersion: 'v1beta1', location: 'global', platformType: 'gcp', }); const agent = createDeepAgent({ model, backend: mySandboxBackend, // any BaseSandbox implementation name: 'test-agent', systemPrompt: 'You are a helpful agent.', }); const stream = await agent.stream( { messages: [new HumanMessage('List files in /workspace')] }, { configurable: { thread_id: 'test-1' }, recursionLimit: 100, streamMode: ['updates', 'messages'], subgraphs: true } ); for await (const event of stream) { /* fails immediately */ } ``` ## Root Cause Analysis The Gemini API strictly requires that every function call in a conversation turn has exactly one matching function response. Other model providers (OpenAI, Anthropic) are more lenient about this. The `FilesystemMiddleware` (and possibly `MemoryMiddleware`/`SkillsMiddleware`) appears to construct message history during initialization that includes tool call/response pairs with mismatched counts. When this history is sent to Gemini's `streamGenerateContent` endpoint, the API rejects it. I verified this is not a checkpoint issue (clean checkpoints, same error) and not caused by `memory`/`skills` parameters (removed them, same error — `FilesystemMiddleware` always runs when `backend` is provided). **Note:** `createDeepAgent()` works fine with Gemini for basic calls without a `backend` — the issue only manifests when `FilesystemMiddleware` is active. ## Related Issues - [langchain-ai/langchainjs#8454](https://github.com/langchain-ai/langchainjs/issues/8454) — `@langchain/google` streaming parser drops parallel tool calls - [google-gemini/gemini-cli#16135](https://github.com/google-gemini/gemini-cli/issues/16135) — Parallel function calling crashes on Gemini 2.x - [google-gemini/gemini-cli#1535](https://github.com/google-gemini/gemini-cli/issues/1535) — Same 400 error in gemini-cli ## Workaround Attempts - Removing `memory` and `skills` params → still fails (FilesystemMiddleware is implicit) - Clearing checkpoint state → still fails (not a stale state issue) - Non-streaming mode → not easily configurable through `createDeepAgent` ## Expected Behavior `createDeepAgent()` should work with Gemini models, ensuring that the message history sent to the model always maintains 1:1 tool call/response parity.
yindo closed this issue 2026-06-05 17:21:15 -04:00
Author
Owner

@Lovakovic commented on GitHub (Mar 17, 2026):

Update: Root Cause Identified + Workaround

After extensive debugging, I can narrow down the root cause more precisely.

Actual Root Cause: createPatchToolCallsMiddleware

The issue is not in FilesystemMiddleware (which has no beforeAgent hook and no init-time tool calls). The culprit is createPatchToolCallsMiddleware, which detects dangling AIMessage tool_calls without corresponding ToolMessages and inserts synthetic cancellation ToolMessages. This is useful for handling HITL interruptions, but it produces a message history that violates Gemini's strict 1:1 tool call/response parity requirement.

Specifically, the message history sent to Gemini on the very first model call contains:

  1. Orphaned ToolMessage responses with no preceding AI message tool calls
  2. AI messages with N tool calls followed by fewer than N ToolMessage responses

Gemini rejects this with 400 INVALID_ARGUMENT.

Evidence from checkpoint inspection

Dumping the checkpoint writes for the first model call shows:

Message 1 [ToolMessage]: TOOL RESPONSE (no preceding AI call)
Message 2 [ToolMessage]: TOOL RESPONSE (no preceding AI call)  
Message 3 [AIMessage]: 2 tool calls: ['ls', 'ls']
Message 4 [AIMessage]: 1 tool call: ['write_todos']
Message 5 [ToolMessage]: 1 response (for 2 calls — mismatch!)

Additional finding: streaming vs non-streaming

This is NOT a streaming-only issue. The same 400 occurs with generateContent (non-streaming) — confirmed by overriding _streamResponseChunks to delegate to _generate. The malformed message history is the cause regardless of streaming mode.

Workaround

We implemented a message sanitizer in a ChatGoogle subclass that runs before every _generate call:

  1. Removes orphaned ToolMessages — those without a preceding AI message containing a matching tool_call_id
  2. Injects placeholder ToolMessages for any AI tool calls that lack matching responses (content: "[No response recorded]")

This ensures Gemini always sees 1:1 parity. With this sanitizer in place, createDeepAgent() with memory, skills, and backend all work correctly with Gemini 3.1 Pro — our RFP parsing agent completes successfully in ~3 minutes.

Suggested fix

createPatchToolCallsMiddleware should either:

  • Be aware of model provider requirements and produce Gemini-compatible message history
  • Or provide an option to disable synthetic ToolMessage injection for providers that enforce strict parity

Happy to provide more details or a PR if helpful.

<!-- gh-comment-id:4075938892 --> @Lovakovic commented on GitHub (Mar 17, 2026): ## Update: Root Cause Identified + Workaround After extensive debugging, I can narrow down the root cause more precisely. ### Actual Root Cause: `createPatchToolCallsMiddleware` The issue is **not** in `FilesystemMiddleware` (which has no `beforeAgent` hook and no init-time tool calls). The culprit is `createPatchToolCallsMiddleware`, which detects dangling AIMessage tool_calls without corresponding ToolMessages and inserts **synthetic cancellation ToolMessages**. This is useful for handling HITL interruptions, but it produces a message history that violates Gemini's strict 1:1 tool call/response parity requirement. Specifically, the message history sent to Gemini on the very first model call contains: 1. Orphaned `ToolMessage` responses with no preceding AI message tool calls 2. AI messages with N tool calls followed by fewer than N `ToolMessage` responses Gemini rejects this with 400 `INVALID_ARGUMENT`. ### Evidence from checkpoint inspection Dumping the checkpoint writes for the first model call shows: ``` Message 1 [ToolMessage]: TOOL RESPONSE (no preceding AI call) Message 2 [ToolMessage]: TOOL RESPONSE (no preceding AI call) Message 3 [AIMessage]: 2 tool calls: ['ls', 'ls'] Message 4 [AIMessage]: 1 tool call: ['write_todos'] Message 5 [ToolMessage]: 1 response (for 2 calls — mismatch!) ``` ### Additional finding: streaming vs non-streaming This is NOT a streaming-only issue. The same 400 occurs with `generateContent` (non-streaming) — confirmed by overriding `_streamResponseChunks` to delegate to `_generate`. The malformed message history is the cause regardless of streaming mode. ### Workaround We implemented a message sanitizer in a `ChatGoogle` subclass that runs before every `_generate` call: 1. **Removes orphaned ToolMessages** — those without a preceding AI message containing a matching `tool_call_id` 2. **Injects placeholder ToolMessages** for any AI tool calls that lack matching responses (content: `"[No response recorded]"`) This ensures Gemini always sees 1:1 parity. With this sanitizer in place, `createDeepAgent()` with `memory`, `skills`, and `backend` all work correctly with Gemini 3.1 Pro — our RFP parsing agent completes successfully in ~3 minutes. ### Suggested fix `createPatchToolCallsMiddleware` should either: - Be aware of model provider requirements and produce Gemini-compatible message history - Or provide an option to disable synthetic ToolMessage injection for providers that enforce strict parity Happy to provide more details or a PR if helpful.
Author
Owner

@pawel-twardziak commented on GitHub (Mar 20, 2026):

hi @Lovakovic thanks for the detailed explanation! Could you look into the PR please? The final decision is of course in the maintainers' hands.

<!-- gh-comment-id:4101438159 --> @pawel-twardziak commented on GitHub (Mar 20, 2026): hi @Lovakovic thanks for the detailed explanation! Could you look into the PR please? The final decision is of course in the maintainers' hands.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/deepagentsjs#247