Skip to content

fix(lingua): skip content ToolUse blocks when tool_calls is non-empty#170

Open
ekeith (evanmkeith) wants to merge 1 commit intomainfrom
04-01-fix(lingua)-deduplicate-tool-calls-from-LangGraph-Anthropic/Bedrock-traces
Open

fix(lingua): skip content ToolUse blocks when tool_calls is non-empty#170
ekeith (evanmkeith) wants to merge 1 commit intomainfrom
04-01-fix(lingua)-deduplicate-tool-calls-from-LangGraph-Anthropic/Bedrock-traces

Conversation

@evanmkeith
Copy link
Copy Markdown

@evanmkeith ekeith (evanmkeith) commented Apr 1, 2026

Summary

LangGraph stores AIMessages in graph state with two representations of the
same tool call: content[].tool_use (Anthropic/Bedrock provider format) and
tool_calls[] (LangChain-native format). parse_assistant_content processed
both without deduplication, emitting two ToolCall parts that rendered as
duplicate entries in the LLM view tab.

Change

In parse_assistant_content (langchain.rs), skip LangChainContentPartCompat::ToolUse
blocks in the content[] branch when tool_calls is non-empty. The
tool_calls[] array is the canonical source in this case. Text parts in
content[] continue to be processed normally.

The early-return path (when tool_calls.is_empty() == true) is unchanged —
providers that only populate content[] are unaffected.

Notes

  • Visual-only bug. Raw span data in brainstore is unaffected.
  • Workaround for affected users: Thread view or JSON view.
  • Related: the surviving entry's arguments may still show a $serde_json::private::Number artifact for integer values due to a serde_wasm_bindgen / serde_json::Value serialization issue — tracked separately.

Testing

  • Existing Lingua tests pass (cargo test)
  • New test: parse_assistant_content with dual-representation input produces exactly one ToolCall part
  • End-to-end: reproduction script at pylon-13952/reproduce_tool_call_duplication.py shows tool call once in LLM view tab

Linear bug here

  ## Summary

  LangGraph stores AIMessages in graph state with two representations of the
  same tool call: `content[].tool_use` (Anthropic/Bedrock provider format) and
  `tool_calls[]` (LangChain-native format). `parse_assistant_content` processed
  both without deduplication, emitting two `ToolCall` parts that rendered as
  duplicate entries in the LLM view tab.

  ## Change

  In `parse_assistant_content` (`langchain.rs`), skip `LangChainContentPartCompat::ToolUse`
  blocks in the `content[]` branch when `tool_calls` is non-empty. The
  `tool_calls[]` array is the canonical source in this case. Text parts in
  `content[]` continue to be processed normally.

  The early-return path (when `tool_calls.is_empty() == true`) is unchanged —
  providers that only populate `content[]` are unaffected.

  ## Notes

  - Visual-only bug. Raw span data in brainstore is unaffected.
  - Workaround for affected users: Thread view or JSON view.
  - Related: the surviving entry's arguments may still show a
    `$serde_json::private::Number` artifact for integer values due to a
    `serde_wasm_bindgen` / `serde_json::Value` serialization issue — tracked
    separately.

  ## Testing

  - [ ] Existing Lingua tests pass (`cargo test`)
  - [ ] New test: `parse_assistant_content` with dual-representation input
    produces exactly one `ToolCall` part
  - [ ] End-to-end: reproduction script at
    `pylon-13952/reproduce_tool_call_duplication.py` shows tool call once in
    LLM view tab

  Fixes Pylon #13952 / BT-4608
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.

1 participant