Skip to content

Add MessageDelta streaming tool-call status helpers#549

Merged
brainlid merged 2 commits into
mainfrom
feat/message-delta-tool-status-helpers
May 13, 2026
Merged

Add MessageDelta streaming tool-call status helpers#549
brainlid merged 2 commits into
mainfrom
feat/message-delta-tool-status-helpers

Conversation

@brainlid
Copy link
Copy Markdown
Owner

@brainlid brainlid commented May 13, 2026

Problem

Streaming consumers (Phoenix LiveView, Sagents) need to track the lifecycle of a tool call as it arrives in pieces across multiple MessageDelta events: first the call is identified, then display_text is filled in, then execution status moves through executingcompleted/failed. Today every consumer reimplements the same merge-and-update logic against delta.tool_calls, including subtle rules like "never overwrite an existing display_text with nil" and "don't clear the in-flight UI until every sibling tool call has terminated." Those rules belong on the library side.

Solution

Add four small, well-documented helpers to LangChain.MessageDelta that codify the merge rules:

  • upsert_tool_call/2 — append a new ToolCall or merge non-nil fields onto an existing one with the same call_id. status is promoted one-way (:incomplete:complete, never back), and metadata is shallow-merged with incoming keys winning per-key.
  • set_tool_execution_status/3 — drive the metadata["execution_status"] lifecycle for a specific call_id, delegating to ToolCall.set_execution_status/2.
  • set_tool_display_text/3 — refine display_text as more is learned ("Reading file""Reading \"outline.md\" (lines 60–100)"). A nil incoming value is an explicit no-op so the UI never regresses from showing something to showing nothing.
  • all_tools_terminal?/1 — gate UI cleanup on every tool call reaching a terminal status. Returns false for nil or empty tool_calls so empty deltas don't trigger premature clears.

All helpers are additive — no existing MessageDelta behavior changes — and they tolerate nil/empty tool_calls so callers don't need to guard.

Changes

  • lib/message_delta.ex — Added upsert_tool_call/2, set_tool_execution_status/3, set_tool_display_text/3, all_tools_terminal?/1, plus private merge_tool_call_fields/2, promote_status/2, merge_metadata/2, and update_matching_tool_call/3 helpers. Each public function carries a @spec and a doctest covering the typical case.
  • test/message_delta_test.exs — Added four describe blocks (one per helper) covering: append vs. merge on upsert_tool_call, nil tool_calls handling, per-field merge precedence, one-way :complete status promotion, shallow metadata merge, set_* no-ops when no matching call_id is present, the nil-display-text no-op rule, and terminal/non-terminal cases for all_tools_terminal?.

Testing

  • New unit tests in test/message_delta_test.exs cover both happy paths and the edge cases that motivated each rule (nil tool_calls, missing call_id, status downgrades, nil display_text).
  • Doctests on each public function exercise the typical streaming usage shape.
  • No live API tests required — these helpers are pure data transforms on MessageDelta.

Run locally with:

cd my_langchain && mix test test/message_delta_test.exs

@brainlid brainlid merged commit 4d8e3c3 into main May 13, 2026
2 checks passed
@brainlid brainlid deleted the feat/message-delta-tool-status-helpers branch May 13, 2026 13:52
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