[PR #335] [MERGED] fix(deepagents): remove orphaned ToolMessages for Gemini compatibility #368

Closed
opened 2026-06-05 17:22:50 -04:00 by yindo · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/langchain-ai/deepagentsjs/pull/335
Author: @pawel-twardziak
Created: 3/20/2026
Status: Merged
Merged: 3/24/2026
Merged by: @christian-bromann

Base: mainHead: fix/patch-tool-calls-orphaned-messages


📝 Commits (2)

  • 2e20aeb fix(deepagents): remove orphaned ToolMessages for Gemini compatibility
  • d87aab3 Fix ToolMessages for Gemini compatibility

📊 Changes

4 files changed (+470 additions, -10 deletions)

View changed files

.changeset/fresh-ways-cross.md (+5 -0)
📝 .gitignore (+1 -0)
📝 libs/deepagents/src/middleware/patch_tool_calls.test.ts (+411 -0)
📝 libs/deepagents/src/middleware/patch_tool_calls.ts (+53 -10)

📄 Description

Summary

  • Enhance patchDanglingToolCalls to enforce bidirectional tool call / tool response parity by also removing orphaned ToolMessages (those whose tool_call_id doesn't match any tool_call in any AIMessage)
  • Fix off-by-one in dangling tool_call search (.slice(i).slice(i + 1) to skip the AIMessage itself)
  • Add 10 new test cases covering orphaned ToolMessage removal, combined orphan+dangling scenarios, and a reproduction of the exact checkpoint state from #314

Fixes #314

Motivation

Google Gemini models strictly require 1:1 parity between function call parts and function response parts. The existing createPatchToolCallsMiddleware only handled one direction - injecting synthetic ToolMessages for dangling tool_calls - but did not remove orphaned ToolMessages that appear without a matching AIMessage tool_call. This caused Gemini to reject requests with 400 INVALID_ARGUMENT on the very first model invocation when createDeepAgent was used with a backend.

This fix is provider-agnostic - orphaned ToolMessages are structurally invalid for all providers (OpenAI, Anthropic, Gemini). Gemini is simply the only one that enforces it with a hard error. The fix improves message hygiene universally without any provider-specific logic.

Changes

patchDanglingToolCalls (patch_tool_calls.ts)

Converted from a single-pass to a two-pass algorithm:

  1. Pass 1: Collect all tool_call_ids from every AIMessage into a Set<string>
  2. Pass 2: Iterate through messages - drop ToolMessages whose tool_call_id is not in the set (orphaned), and inject synthetic cancellation ToolMessages for tool_calls without responses (dangling, existing behavior)

The middleware factory (createPatchToolCallsMiddleware) is unchanged - it already delegates to patchDanglingToolCalls in both beforeAgent and wrapModelCall.

Tests (patch_tool_calls.test.ts)

Added 10 new test cases across 4 describe blocks:

Section Tests
orphaned ToolMessages (should remove) Orphans at start, middle, mixed with valid ToolMessages
combined orphaned + dangling (Gemini issue #314) Exact checkpoint dump reproduction with full parity assertion, clean state pass-through
wrapModelCall Orphan removal, combined orphan+dangling
patchDanglingToolCalls utility Orphan removal, combined, strict 1:1 parity for multi-tool-call AIMessages

All 26 tests pass (16 existing + 10 new). All 710 tests in the package pass.

Test plan

  • All existing patchDanglingToolCalls tests continue to pass (backward compatibility)
  • New orphaned ToolMessage removal tests pass across beforeAgent, wrapModelCall, and direct utility function
  • #314 checkpoint dump scenario produces strict 1:1 parity (every tool_call has exactly one ToolMessage, no ToolMessage exists without a matching tool_call)
  • Full test suite passes (npx vitest run - 710 tests, 24 files)
  • Manual verification with ChatGoogle + createDeepAgent + backend (requires Vertex AI credentials)

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/langchain-ai/deepagentsjs/pull/335 **Author:** [@pawel-twardziak](https://github.com/pawel-twardziak) **Created:** 3/20/2026 **Status:** ✅ Merged **Merged:** 3/24/2026 **Merged by:** [@christian-bromann](https://github.com/christian-bromann) **Base:** `main` ← **Head:** `fix/patch-tool-calls-orphaned-messages` --- ### 📝 Commits (2) - [`2e20aeb`](https://github.com/langchain-ai/deepagentsjs/commit/2e20aeb5646f526504d531f3dd9e7000977e5e97) fix(deepagents): remove orphaned ToolMessages for Gemini compatibility - [`d87aab3`](https://github.com/langchain-ai/deepagentsjs/commit/d87aab334c13f57d9578036278a12e099a22017c) Fix ToolMessages for Gemini compatibility ### 📊 Changes **4 files changed** (+470 additions, -10 deletions) <details> <summary>View changed files</summary> ➕ `.changeset/fresh-ways-cross.md` (+5 -0) 📝 `.gitignore` (+1 -0) 📝 `libs/deepagents/src/middleware/patch_tool_calls.test.ts` (+411 -0) 📝 `libs/deepagents/src/middleware/patch_tool_calls.ts` (+53 -10) </details> ### 📄 Description ## Summary - Enhance `patchDanglingToolCalls` to enforce **bidirectional** tool call / tool response parity by also removing orphaned `ToolMessage`s (those whose `tool_call_id` doesn't match any `tool_call` in any `AIMessage`) - Fix off-by-one in dangling tool_call search (`.slice(i)` → `.slice(i + 1)` to skip the `AIMessage` itself) - Add 10 new test cases covering orphaned ToolMessage removal, combined orphan+dangling scenarios, and a reproduction of the exact checkpoint state from #314 Fixes #314 ## Motivation Google Gemini models strictly require 1:1 parity between function call parts and function response parts. The existing `createPatchToolCallsMiddleware` only handled one direction - injecting synthetic `ToolMessage`s for dangling `tool_call`s - but did not remove orphaned `ToolMessage`s that appear without a matching `AIMessage` `tool_call`. This caused Gemini to reject requests with `400 INVALID_ARGUMENT` on the very first model invocation when `createDeepAgent` was used with a `backend`. This fix is **provider-agnostic** - orphaned `ToolMessage`s are structurally invalid for all providers (OpenAI, Anthropic, Gemini). Gemini is simply the only one that enforces it with a hard error. The fix improves message hygiene universally without any provider-specific logic. ## Changes ### `patchDanglingToolCalls` (`patch_tool_calls.ts`) Converted from a single-pass to a two-pass algorithm: 1. **Pass 1**: Collect all `tool_call_id`s from every `AIMessage` into a `Set<string>` 2. **Pass 2**: Iterate through messages - drop `ToolMessage`s whose `tool_call_id` is not in the set (orphaned), and inject synthetic cancellation `ToolMessage`s for `tool_call`s without responses (dangling, existing behavior) The middleware factory (`createPatchToolCallsMiddleware`) is unchanged - it already delegates to `patchDanglingToolCalls` in both `beforeAgent` and `wrapModelCall`. ### Tests (`patch_tool_calls.test.ts`) Added 10 new test cases across 4 `describe` blocks: | Section | Tests | |---|---| | `orphaned ToolMessages (should remove)` | Orphans at start, middle, mixed with valid ToolMessages | | `combined orphaned + dangling (Gemini issue #314)` | Exact checkpoint dump reproduction with full parity assertion, clean state pass-through | | `wrapModelCall` | Orphan removal, combined orphan+dangling | | `patchDanglingToolCalls utility` | Orphan removal, combined, strict 1:1 parity for multi-tool-call AIMessages | All 26 tests pass (16 existing + 10 new). All 710 tests in the package pass. ## Test plan - [x] All existing `patchDanglingToolCalls` tests continue to pass (backward compatibility) - [x] New orphaned ToolMessage removal tests pass across `beforeAgent`, `wrapModelCall`, and direct utility function - [x] #314 checkpoint dump scenario produces strict 1:1 parity (every tool_call has exactly one ToolMessage, no ToolMessage exists without a matching tool_call) - [x] Full test suite passes (`npx vitest run` - 710 tests, 24 files) - [ ] Manual verification with `ChatGoogle` + `createDeepAgent` + `backend` (requires Vertex AI credentials) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
yindo added the pull-request label 2026-06-05 17:22:50 -04:00
yindo closed this issue 2026-06-05 17:22:50 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/deepagentsjs#368