Skip to content

fix(react-headless): order LangGraph multi-step tool loops correctly - #966

Open
AbhinRustagi wants to merge 2 commits into
be-framework-docsfrom
be-framework-adapter
Open

fix(react-headless): order LangGraph multi-step tool loops correctly#966
AbhinRustagi wants to merge 2 commits into
be-framework-docsfrom
be-framework-adapter

Conversation

@AbhinRustagi

@AbhinRustagi AbhinRustagi commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What

Two bugs in the same tool-loop path — one in how LangGraph streams are decoded, one in how any provider-executed tool activity is rendered.

Stream. The LangGraph adapter assumed one assistant message per stream. A graph that called a tool and then resumed the model collapsed into a single merged message with no tool results, so what you saw live didn't match what the next request replayed.

Render. A turn whose tool activity is all provider-executed crashed the thread with can't access property "type", current is undefined. An OpenUI Cloud scaffold reproduces it.

Changes

react-headless — LangGraph adapter

  • Track the current message id per graph step, so each model step becomes its own assistant message.
  • Handle ToolMessage / ToolMessageChunk — a result closes the open tool call and the assistant segment before emitting TOOL_CALL_RESULT, keeping history ordered ai → tool → ai.
  • Prefer tool_call_chunks over tool_calls when both are present. LangChain chunks carry a provisional tool_calls projection, often with empty args, which duplicated calls.
  • Track open and started tool calls separately so each call ends exactly once.
  • Mark status: "error" tool messages as errored results.

react-ui — timeline rendering

  • Thread built its rows only from tool calls it could match back to an assistant segment, silently discarding the orphan activities pairToolActivity synthesizes for tool results whose call lives on no assistant message. Provider-executed tools are exactly that shape, so their activity never rendered — and when a turn had only orphans, steps came back empty while turnActivities did not.
  • ToolCallTimeline guarded on activities but indexed steps, so that combination evaluated steps[-1] and threw. Unclaimed activities are now appended as rows, and the guard covers the array actually indexed.

Test Plan

  • Verified locally

react-headless 168/168 and react-ui 29/29 passing; prettier and tsc clean. Adds unit coverage plus an integration test asserting a tool-loop turn stays ordered and valid for the next request.

The render fix is verified by the existing suites plus tracing the failure end to end — pairToolActivity synthesizes orphans (already covered by its own tests), Thread dropped them, ToolCallTimeline then indexed [-1]. It has no direct regression test, since this repo's convention is not to add test files unprompted.

Checklist

  • I considered backwards compatibility

@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openui-docs Ready Ready Preview Aug 14, 2026 11:20am

Request Review

AbhinRustagi and others added 2 commits August 14, 2026 16:47
The LangGraph adapter assumed one assistant message per stream, so a
graph that called a tool and then resumed the model emitted a single
merged message with no tool results. Live history diverged from what the
next request replayed.

- Track the current message id per graph step instead of one id for the
  whole stream, so each model step becomes its own assistant message.
- Handle `ToolMessage` / `ToolMessageChunk`: a tool result now closes the
  open tool call and the assistant segment before emitting
  TOOL_CALL_RESULT, keeping live history ordered ai -> tool -> ai.
- Prefer `tool_call_chunks` over `tool_calls` when both are present.
  LangChain chunks carry a provisional `tool_calls` projection (often
  with empty args) that duplicated calls and emitted empty arguments.
- Track open vs started tool calls separately so a call is ended exactly
  once, whether it closes on a tool result or at end of stream.
- Mark `status: "error"` tool messages as errored results.

Also fixes a crash on the rendering side, hit by any backend whose tools
are provider-executed (an OpenUI Cloud scaffold reproduces it):

- `Thread` built its timeline rows only from tool calls it could match
  back to an assistant segment, silently dropping the orphan activities
  `pairToolActivity` synthesizes for tool results whose call lives on no
  assistant message. Those activities never rendered, and when a turn had
  only orphans the row list came back empty while the activity list did
  not.
- `ToolCallTimeline` guarded on `activities` but indexed `steps`, so that
  combination evaluated `steps[-1]` and threw "current is undefined".
  Append unclaimed activities as rows, and guard the array it indexes.

Adds unit coverage plus an integration test asserting a tool-loop turn
stays ordered and replayable.

Migrated from PR #785 (visharad/th-2051-route-replacement).

Co-Authored-By: Visharad Kashyap <154831195+vishxrad@users.noreply.github.com>
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