fix(aws-strands): correct parallel tool-call history and defer frontend hand-off - #2343
Conversation
…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>
|
Rebuilding on All of Fix #4 confirmed
Fix #3 flush placement — correct, including the constraintFlush 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 Two things1. 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 Now that #2334 has landed the halt-path teardown this was entangled with, moving it into 2. Merge orderingClean against TestsEight, including |
Python Preview PackagesVersion
Install with uvAdd the TestPyPI index to your [[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = trueThen 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 testpypiInstall with pippip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
ag-ui-protocol==0.0.0.dev1786148016
Commit: d47aad4 |
@ag-ui/a2a-middleware
@ag-ui/a2ui-middleware
@ag-ui/event-throttle-middleware
@ag-ui/mcp-apps-middleware
@ag-ui/mcp-middleware
@ag-ui/a2a
@ag-ui/adk
@ag-ui/ag2
@ag-ui/agno
@ag-ui/aws-strands
@ag-ui/claude-agent-sdk
@ag-ui/claude-managed-agents
@ag-ui/crewai
@ag-ui/langchain
@ag-ui/langgraph
@ag-ui/llamaindex
@ag-ui/mastra
@ag-ui/pydantic-ai
@ag-ui/vercel-ai-sdk
@ag-ui/watsonx
@ag-ui/a2ui-toolkit
create-ag-ui-app
@ag-ui/client
@ag-ui/core
@ag-ui/encoder
@ag-ui/proto
commit: |
contextablemark
left a comment
There was a problem hiding this comment.
@YiyuanMiao Thanks so much for this and for enduring the issues with conflict resoluton! Approved.
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_turnsCalled at the end of
_build_strands_historyto normalize the tool history so Bedrock'stoolUse/toolResultpairing holds. Three problems fixed:RecursionErroron large histories (fine at ~800 tool turns, failed at ~1200). Rewritten as an iterative loop; verified to 20,000 turns.toolUseId. A repeated id previously emitted a duplicatetoolResult, which Bedrock rejects. Ids are now de-duplicated (first-seen order preserved).toolUse/toolResultpair 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).ToolCallEndinstead of emitting it immediately.TOOL_CALL_RESULT(s), so the wire order is backend result → frontend end.TOOL_CALL_STARTleft without a matchingEND).Relationship to #2334
#2334owns the halt-path work (fix #1break/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 inmain. The one shared prerequisite (not discarding the tool-result message in thepending_haltbranch) is already inmainvia #2334.Not included: the
args_streamerpath 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:test_normalize_tool_turns_*— recursion (large history), dup-id dedup, follow-message ordering (Fix Fix the README file to improve visual appeal. #4)test_strands_history_*— end-to-end history bundling/reordering/adjacencyTestDeferredFrontendEndFlushOrder— backend result precedes frontend end (Fix trying to find websockets transport for ag-ui #3)TestParallelFrontendToolCallsAllEmitted/TestContinuationTurnEmitsNewToolCalls— parallel frontend coverage
Full
aws-strandssuite green (194 passed, 2 skipped).