You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
DanglingToolCallMiddleware currently assumes that a tool_call_id is globally
unique across the entire message history when it normalizes tool-call
transcripts before a model call.
That assumption is too strong. OpenAI-compatible providers may reuse the same
tool-call ID string in different assistant turns. A transcript can still be
valid when each assistant tool-call turn is immediately followed by its own
matching ToolMessages.
When repeated IDs exist in history, the current middleware can turn a valid
transcript into an invalid one and trigger a provider 400:
Invalid request: an assistant message with 'tool_calls' must be followed by
tool messages responding to each 'tool_call_id'.
Historical Context
This issue was exposed while validating PR #2883, which changes the
summarization path to hide internal summarization output from the frontend
during context compression.
The summarization middleware is not the direct cause of the invalid transcript.
However, summarization makes the repeated-ID case easier to reach:
Context compression replaces older history with a summary message.
It preserves a recent valid tool-call transcript so the model keeps the
necessary recent context.
A later assistant turn may receive tool-call IDs from the provider/model
adapter that reuse ID strings already present in the preserved transcript.
For example, a preserved turn may already contain web_search:11, and a later
assistant turn may also produce web_search:11.
This is a valid history shape as long as each assistant turn is paired with its
own tool results.
Current Behavior
Before middleware normalization, the history may be valid:
DanglingToolCallMiddleware collects existing tool results by tool_call_id. With a single-value map, repeated IDs collapse to one ToolMessage. During regrouping, the later assistant turn can lose its matching
tool result:
Other paths may also contain repeated tool-call ID strings across turns, so
the fix should live in the middleware that normalizes the transcript rather
than relying on summarization to avoid the case.
Proposed Fix
Make DanglingToolCallMiddleware treat tool_call_id as scoped to a tool-call
occurrence/assistant turn instead of globally unique across the full message
history.
The minimal fix is to preserve all matching ToolMessages for a repeated ID and
consume them in occurrence order during transcript normalization, for example:
tool_call_id->queue[ToolMessage]
instead of:
tool_call_id->ToolMessage
This allows each assistant turn that reuses an ID string to keep its own matching
tool result.
Acceptance Criteria
DanglingToolCallMiddleware does not assume tool_call_id is globally unique
across the full message history.
A valid transcript with repeated tool-call ID strings across separate
assistant turns remains unchanged after normalization.
A non-adjacent transcript with repeated IDs is regrouped without dropping a
later matching ToolMessage.
Existing dangling-tool-call repair behavior remains intact when a tool result
is genuinely missing.
Tests cover the summarization-adjacent shape where preserved history and a
later assistant turn reuse a tool-call ID string.
Notes
A possible defensive enhancement in the summarization path is to rewrite IDs in
preserved tool-call history while updating both assistant tool calls and matching
tool messages. That is not required for the minimal fix: repeated IDs across
assistant turns are valid input, and the normalization middleware should handle
them correctly.
Problem
DanglingToolCallMiddlewarecurrently assumes that atool_call_idis globallyunique across the entire message history when it normalizes tool-call
transcripts before a model call.
That assumption is too strong. OpenAI-compatible providers may reuse the same
tool-call ID string in different assistant turns. A transcript can still be
valid when each assistant tool-call turn is immediately followed by its own
matching
ToolMessages.When repeated IDs exist in history, the current middleware can turn a valid
transcript into an invalid one and trigger a provider 400:
Historical Context
This issue was exposed while validating PR #2883, which changes the
summarization path to hide internal summarization output from the frontend
during context compression.
The summarization middleware is not the direct cause of the invalid transcript.
However, summarization makes the repeated-ID case easier to reach:
necessary recent context.
adapter that reuse ID strings already present in the preserved transcript.
For example, a preserved turn may already contain
web_search:11, and a laterassistant turn may also produce
web_search:11.This is a valid history shape as long as each assistant turn is paired with its
own tool results.
Current Behavior
Before middleware normalization, the history may be valid:
DanglingToolCallMiddlewarecollects existing tool results bytool_call_id. With a single-value map, repeated IDs collapse to oneToolMessage. During regrouping, the later assistant turn can lose its matchingtool result:
The malformed transcript is then sent to the provider, which rejects it.
Why This Matters
transcripts before provider serialization.
repeated-ID case must be handled for the summarization flow to work reliably.
the fix should live in the middleware that normalizes the transcript rather
than relying on summarization to avoid the case.
Proposed Fix
Make
DanglingToolCallMiddlewaretreattool_call_idas scoped to a tool-calloccurrence/assistant turn instead of globally unique across the full message
history.
The minimal fix is to preserve all matching
ToolMessages for a repeated ID andconsume them in occurrence order during transcript normalization, for example:
instead of:
This allows each assistant turn that reuses an ID string to keep its own matching
tool result.
Acceptance Criteria
DanglingToolCallMiddlewaredoes not assumetool_call_idis globally uniqueacross the full message history.
assistant turns remains unchanged after normalization.
later matching
ToolMessage.is genuinely missing.
later assistant turn reuse a tool-call ID string.
Notes
A possible defensive enhancement in the summarization path is to rewrite IDs in
preserved tool-call history while updating both assistant tool calls and matching
tool messages. That is not required for the minimal fix: repeated IDs across
assistant turns are valid input, and the normalization middleware should handle
them correctly.