Skip to content

fix(core): strip null id/name from tool-call-chunk deltas in compat bridge#36962

Open
Christian Bromann (christian-bromann) wants to merge 2 commits intonh/middleware-transformerfrom
cb/new-streaming
Open

fix(core): strip null id/name from tool-call-chunk deltas in compat bridge#36962
Christian Bromann (christian-bromann) wants to merge 2 commits intonh/middleware-transformerfrom
cb/new-streaming

Conversation

@christian-bromann
Copy link
Copy Markdown
Member

Many provider integrations (notably Anthropic's input_json_delta path) attach the tool-call id and name only to the first tool_use chunk; subsequent per-chunk slices carry id=None, name=None and just the fresh args segment. The compat bridge forwarded those None values verbatim, producing wire payloads like {"type": "tool_call_chunk", "id": null, "name": null, "args": "..."}.

Consumers that fold deltas via a naive {...target, ...delta} spread (e.g. the langgraph-js SDK's MessageAssembler.applyContentDelta) interpret those as "identifier reset to null" and lose the id/name captured from content-block-start. Downstream extractors then drop the chunk until the final content-block-finish arrives — visible to end users as tool-call cards appearing all-at-once at the end of a turn instead of streaming in incrementally (the Deep Agent example rendering four subagents in a single flicker rather than one after another).

Introduce _to_protocol_delta_block and route every content-block-delta emission (sync / async chunk streams and the message_to_events replay path) through it. For tool_call_chunk and server_tool_call_chunk shapes, drop id / name keys when they would serialize to null. This matches the wire shape produced by langgraph-js's toProtocolDeltaBlock, where identifiers are only surfaced when they carry a real value.

…ridge

Many provider integrations (notably Anthropic's `input_json_delta`
path) attach the tool-call `id` and `name` only to the first
`tool_use` chunk; subsequent per-chunk slices carry `id=None,
name=None` and just the fresh `args` segment. The compat bridge
forwarded those `None` values verbatim, producing wire payloads like
`{"type": "tool_call_chunk", "id": null, "name": null, "args": "..."}`.

Consumers that fold deltas via a naive `{...target, ...delta}` spread
(e.g. the langgraph-js SDK's `MessageAssembler.applyContentDelta`)
interpret those as "identifier reset to null" and lose the id/name
captured from `content-block-start`. Downstream extractors then drop
the chunk until the final `content-block-finish` arrives — visible to
end users as tool-call cards appearing all-at-once at the end of a
turn instead of streaming in incrementally (the Deep Agent example
rendering four subagents in a single flicker rather than one after
another).

Introduce `_to_protocol_delta_block` and route every
`content-block-delta` emission (sync / async chunk streams and the
`message_to_events` replay path) through it. For `tool_call_chunk`
and `server_tool_call_chunk` shapes, drop `id` / `name` keys when
they would serialize to `null`. This matches the wire shape produced
by langgraph-js's `toProtocolDeltaBlock`, where identifiers are only
surfaced when they carry a real value.
@github-actions github-actions Bot added core `langchain-core` package issues & PRs fix For PRs that implement a fix internal size: S 50-199 LOC labels Apr 23, 2026
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 23, 2026

Merging this PR will not alter performance

✅ 13 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing cb/new-streaming (bd8ab55) with nh/middleware-transformer (494a472)2

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on nh/middleware-transformer (03e2eb7) during the generation of this report, so 4c32454 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

- Rename content-block imports to new protocol names (TextContentBlock,
  ReasoningContentBlock, InvalidToolCall, ToolCall, ToolCallChunk,
  ServerToolCall, ServerToolCallChunk).
- Drop FinishReason and _normalize_finish_reason: the protocol removed
  ``reason`` from ``MessageFinishData`` in
  langchain-ai/protocol@2ef8585.
  Provider-level ``finish_reason`` / ``stop_reason`` now pass through
  verbatim on ``MessageFinishData.metadata`` for downstream consumers.
- Simplify ``_build_message_finish`` and ``_finish_all_blocks``: the
  tool_use re-classification previously driven by the finish reason is
  obsolete now that the wire field is gone.
- Drop the ``_finish_reason`` accumulator from chat_model_stream: the
  same data is surfaced via ``response_metadata`` through the passed-
  through finish metadata.

Made-with: Cursor
@github-actions github-actions Bot added size: M 200-499 LOC and removed size: S 50-199 LOC labels Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core `langchain-core` package issues & PRs fix For PRs that implement a fix internal size: M 200-499 LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant