Skip to content

fix(aws-strands): correct parallel tool-call history and defer frontend hand-off - #2343

Merged
contextablemark merged 2 commits into
ag-ui-protocol:mainfrom
YiyuanMiao:fix-parallel-toolcall-clean
Aug 8, 2026
Merged

fix(aws-strands): correct parallel tool-call history and defer frontend hand-off#2343
contextablemark merged 2 commits into
ag-ui-protocol:mainfrom
YiyuanMiao:fix-parallel-toolcall-clean

Conversation

@YiyuanMiao

Copy link
Copy Markdown
Contributor

Description

Supersedes #2172, rebuilt cleanly on top of #2334 (which landed the halt-path fixes #1/#2). This PR carries only the two independent pieces that #2334 did
not touch — so there's no overlap with it.

Fix #4_normalize_tool_turns

Called at the end of _build_strands_history to normalize the tool history so Bedrock's toolUse/toolResult pairing holds. Three problems fixed:

  • Recursion → iteration. The previous recursive merge raised RecursionError on large histories (fine at ~800 tool turns, failed at ~1200). Rewritten as an iterative loop; verified to 20,000 turns.
  • Duplicate toolUseId. A repeated id previously emitted a duplicate toolResult, which Bedrock rejects. Ids are now de-duplicated (first-seen order preserved).
  • Reordering. Messages that legitimately follow a completed toolUse/toolResult pair are now preserved in place; only messages wedged between the turn and its results are dropped (as the docstring intends).

Fix #3 — defer frontend hand-off

When a turn mixes a frontend tool call with backend tool results, the frontend TOOL_CALL_END (the client's "execute this tool now" signal) must not reach the client before the backend results do — otherwise the client dispatches its follow-up run early and races the current one (ConcurrencyException).

  • Buffer a frontend tool's ToolCallEnd instead of emitting it immediately.
  • Flush the buffer after the turn's backend TOOL_CALL_RESULT(s), so the wire order is backend result → frontend end.
  • Safety flush at end of stream for turns with only frontend tool calls, so a buffered end is never lost (no TOOL_CALL_START left without a matching END).

Relationship to #2334

#2334 owns the halt-path work (fix #1 break/aclose, fix #2 keep-message). This PR builds directly on it and is complementary — it adds the frontend hand-off deferral and the history normalization, neither of which exists in main. The one shared prerequisite (not discarding the tool-result message in the pending_halt branch) is already in main via #2334.

Not included: the args_streamer path still emits a frontend end immediately (a second, non-buffered emission site). It doesn't lose ends, so it's left as a follow-up rather than folded in here.

Tests

test_parallel_tool_call_handling.py:

Full aws-strands suite green (194 passed, 2 skipped).

…nd hand-off

Rebuilt cleanly on top of ag-ui-protocol#2334 (which landed the halt-path fixes ag-ui-protocol#1/ag-ui-protocol#2).
This change carries only the two independent pieces:

Fix ag-ui-protocol#4 — _normalize_tool_turns (called at the end of _build_strands_history):
- iterative merge (no RecursionError on large histories, ~1000+ tool turns)
- de-duplicate toolUseIds so a repeated id never emits a duplicate toolResult
  (Bedrock rejects that)
- preserve messages that legitimately follow a completed toolUse/toolResult
  pair in place; only drop messages wedged between the turn and its results

Fix ag-ui-protocol#3 — defer frontend hand-off:
- buffer a frontend tool's ToolCallEnd instead of emitting it immediately
- flush the buffer after the turn's backend TOOL_CALL_RESULT(s), so the wire
  order is backend result -> frontend end and the client only starts the
  frontend tool once backend work has reached it
- safety flush at end of stream for turns with only frontend tool calls, so a
  buffered end is never lost (no TOOL_CALL_START left without a matching END)

Adds regression tests: normalize recursion/dedup/follow-ordering and the
deferred-end flush order. Full aws-strands suite green.

Signed-off-by: YiyuanMiao <miaoyiyuan31@gmail.com>
@YiyuanMiao
YiyuanMiao requested a review from a team as a code owner August 7, 2026 03:33
@contextablemark

Copy link
Copy Markdown
Contributor

Rebuilding on main rather than rebasing #2172 was the right call — and you'd worked out the flush-before-break resolution independently, an hour and a half before I posted mine on the old PR. Verified against this build:

All of Fix #4 confirmed

  • Recursion → iteration — zero self-recursive calls remain. You verified to 20,000 turns; I'd measured failure at ~1200, so that's a wider margin than I asked for.
  • Dedupseen_ids with first-seen order preserved.
  • Ordering"Continue with whatever legitimately follows, in place (no reordering)", and the docstring now distinguishes wedged between from legitimately follows. That distinction was the thing missing before.

Fix #3 flush placement — correct, including the constraint

Flush at 1537, break at 1550, with the ordering requirement stated in the comment. That matters more than it looks: breaking first would skip the flush and strand every TOOL_CALL_START without a matching END — exactly what the safety flush exists to catch.

Two things

1. The safety flush doesn't hold the guarantee the description claims. The body says it's there "so a buffered end is never lost." It sits inside the try, before finally — so any exception from the async for skips it, and the buffered ends are lost precisely when a TOOL_CALL_START is most likely to be left dangling. It holds for clean stream termination, not for the failure case.

Now that #2334 has landed the halt-path teardown this was entangled with, moving it into finally (or its own try/finally) is unblocked. Either fold it in here, or take it with the args_streamer follow-up — but I'd soften the claim in the description if it stays as-is.

2. args_streamer — agreed, and thanks for calling it out yourself. "a second, non-buffered emission site … doesn't lose ends, so it's left as a follow-up." Correct on both counts: line 2023 emits unconditionally with no is_frontend_tool check, and the failure mode is early hand-off rather than a lost end. Happy for it to be a follow-up. Worth noting the race it reintroduces is the same ConcurrencyException this PR exists to prevent, so it shouldn't drift too far.

Merge ordering

Clean against main. Conflicts with #2286 — both add a variable declaration at the same point in the run-state block (deferred_frontend_tool_ends vs terminal_result). Trivial, both survive, but whoever lands second rebases. Given #2286 still has two open blockers and this is ready, I'd suggest this goes first.

Tests

Eight, including test_backend_result_precedes_frontend_end and test_every_start_has_matching_end — the two that actually pin Fix #3's wire-order guarantee, rather than just its mechanics.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Python Preview Packages

Version 0.0.0.dev1786148016 published to TestPyPI.

Warning: These packages are built from contributor code that may not yet have been vetted for correctness or security. Install at your own risk and do not use in production.

Install with uv

Add the TestPyPI index to your pyproject.toml:

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = true

Then install the packages you need:

# Core SDK
uv add 'ag-ui-protocol==0.0.0.dev1786148016' --index testpypi

# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1786148016' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1786148016' --index testpypi
# NOTE: ag-ui-agent-spec depends on pyagentspec (git-only, not on PyPI).
# You will need to install pyagentspec separately from its git repo.
uv add 'ag-ui-agent-spec==0.0.0.dev1786148016' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1786148016' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1786148016' --index testpypi

Install with pip

pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  ag-ui-protocol==0.0.0.dev1786148016

Use --extra-index-url https://pypi.org/simple/ so pip can resolve
transitive dependencies (pydantic, fastapi, etc.) from real PyPI.


Commit: d47aad4

@pkg-pr-new

pkg-pr-new Bot commented Aug 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

@ag-ui/a2a-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a-middleware@2343

@ag-ui/a2ui-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-middleware@2343

@ag-ui/event-throttle-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/event-throttle-middleware@2343

@ag-ui/mcp-apps-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-apps-middleware@2343

@ag-ui/mcp-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-middleware@2343

@ag-ui/a2a

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a@2343

@ag-ui/adk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/adk@2343

@ag-ui/ag2

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/ag2@2343

@ag-ui/agno

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/agno@2343

@ag-ui/aws-strands

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/aws-strands@2343

@ag-ui/claude-agent-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-agent-sdk@2343

@ag-ui/claude-managed-agents

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-managed-agents@2343

@ag-ui/crewai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/crewai@2343

@ag-ui/langchain

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langchain@2343

@ag-ui/langgraph

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langgraph@2343

@ag-ui/llamaindex

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/llamaindex@2343

@ag-ui/mastra

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mastra@2343

@ag-ui/pydantic-ai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/pydantic-ai@2343

@ag-ui/vercel-ai-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/vercel-ai-sdk@2343

@ag-ui/watsonx

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/watsonx@2343

@ag-ui/a2ui-toolkit

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-toolkit@2343

create-ag-ui-app

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/create-ag-ui-app@2343

@ag-ui/client

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/client@2343

@ag-ui/core

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/core@2343

@ag-ui/encoder

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/encoder@2343

@ag-ui/proto

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/proto@2343

commit: 176c543

@contextablemark contextablemark left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@YiyuanMiao Thanks so much for this and for enduring the issues with conflict resoluton! Approved.

@contextablemark
contextablemark merged commit 68b99d8 into ag-ui-protocol:main Aug 8, 2026
44 checks passed
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.

trying to find websockets transport for ag-ui

2 participants