Repository navigation
Python: [Bug]: with predict_state_config set, EVERY tool result emits a full StateSnapshotEvent — including for tools the config never names #7490
Description
Activity
- addedpythonUsage: [Issues, PRs], Target: PythonUsage: [Issues, PRs], Target: PythontriageUsage: [Issues], Target: All issues that still need to be triagedUsage: [Issues], Target: All issues that still need to be triaged
on Aug 3, 2026 - addedneeds-maintainer-triageUsage: [Issues], Target: all issues that the automated triage flow failed to processUsage: [Issues], Target: all issues that the automated triage flow failed to process
on Aug 3, 2026 - addedag-uiUsage: [Issues, PRs], Target: AG-UI protocol integrationUsage: [Issues, PRs], Target: AG-UI protocol integrationand removedtriageUsage: [Issues], Target: All issues that still need to be triagedUsage: [Issues], Target: All issues that still need to be triagedneeds-maintainer-triageUsage: [Issues], Target: all issues that the automated triage flow failed to processUsage: [Issues], Target: all issues that the automated triage flow failed to process
on Aug 4, 2026 - addedreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflowUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
on Aug 4, 2026 🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
Bug is in
_emit_tool_result_common()inpython/packages/ag-ui/agent_framework_ag_ui/_run_common.pyat line 759: the guardif (predictive_handler or state_update)is always true when anypredict_state_configis set becausepredictive_handleris a truthy object, even whenapply_pending_updates()was a no-op. Minimal repro: construct aPredictiveStateHandlerconfigured for tool "update_mode", then call_emit_tool_result_commonwith that handler for a different tool name (e.g. "look_up") andstate_update=None— a spuriousStateSnapshotEventis emitted despite no state change. The fix should check whetherpending_state_updateswas non-empty beforeapply_pending_updates()cleared it, or haveapply_pending_updates()return a boolean.- Failing test:
python/packages/ag-ui/tests/ag_ui/test_bug_7490_spurious_snapshot.py - Files examined: python/packages/ag-ui/agent_framework_ag_ui/_run_common.py, python/packages/ag-ui/agent_framework_ag_ui/_predictive_state.py, python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py, python/packages/ag-ui/pyproject.toml
- Tests run: python/packages/ag-ui/tests/ag_ui/test_bug_7490_spurious_snapshot.py (5 tests: 2 FAILED, 3 PASSED)
- Reported version:
1.0.1 - Current version:
1.0.1
- Failing test:
Confirmed on current
mainat064751c. A configuredPredictiveStateHandlerwith an emptypending_state_updatesstill makes_emit_tool_result()emit oneStateSnapshotEventcontaining unchanged state.I can take this. I’d keep the fix local to
_emit_tool_result_common(): capture whether pending updates exist beforeapply_pending_updates()clears them, then gate the coalesced snapshot on that boolean orstate_update. I’d add regression coverage for both sides: an unrelated tool emits no snapshot, while a real pending update still emits exactly one.I’d leave the broader snapshot-vs-delta question out of this fix. Evan Mattson (@moonbox3), if you’re already working on it, I’ll stand down; otherwise I’m happy to send the focused PR.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Description
1. Symptom
An AG-UI app that configures
predict_state_configfor a single tool emits oneStateSnapshotEvent— carrying the whole shared state — after every tool result in the run, no matter which tool ran. In a deployed app with a 16-key-deep shared state, that was 1,632 snapshots against 1,600TOOL_CALL_STARTevents over two days: a 1:1 ratio with tool calls, not with predictive-state activity (the predictive tool was called a handful of times).Because each snapshot serializes the entire
flow.current_state, the per-tool-call cost grows with the conversation: in one 95-minute session the snapshots grew 7 KB → 15.9 KB each, andStateSnapshotEventalone accounted for 44% of all bytes the server streamed to the browser (STATE_SNAPSHOT+STATE_DELTAtogether: 71%; assistant text: 3%).2. Cause
_emit_tool_result_commondecides whether to emit the snapshot by testing whether a predictive handler exists, not whether it updated anything — as its own comment says it intends (python/packages/ag-ui/agent_framework_ag_ui/_run_common.py:735-748):The handler is constructed once per run whenever the app passes any config at all (
_agent_run.py:1947-1952):and it only ever accumulates work for its configured tool — both delta paths skip everything else (
_orchestration/_predictive_state.py:142-143and:177-178):So for any other tool,
pending_state_updatesis empty,apply_pending_updates()is a no-op (:228-232), and the emit still fires. The information needed to make the condition match the comment is already in hand:pending_state_updatesis non-empty exactly when the predictive mechanism changed state.3. Minimal repro
docs/issues/repros/repro_agui_predictive_snapshot_per_tool_result.pyin this repository — public API only (AgentFrameworkAgent.run), deterministic, no LLM and no network. A fake chat client callslook_up(never the predictive toolupdate_mode), and the run is executed twice, differing only in whetherpredict_state_configis passed:One snapshot in each run is the run-boundary one both configurations emit; the remainder are one per tool result, and they scale exactly with the number of tool calls.
update_modeis never invoked in either run.4. Impact
Every extra snapshot is a full re-serialization of shared state on the server, a full re-transmission over SSE, and — on an AG-UI client — a state-changed notification that re-renders whatever subscribes to it. All three costs are borne per tool call and scale with the size of the shared state, which typically grows with the conversation. The measurements in §1 come from a production deployment; the effect a user reports is a UI that gets progressively slower the longer a conversation runs.
Two properties make this hard for an app to mitigate on its own: the emit happens inside the event pipeline, below anything the app can intercept, and opting out means giving up predictive state entirely for the one tool that legitimately uses it.
Code Sample
Error Messages / Stack Traces
Package Versions
agent-framework-ag-ui: 1.0.1, agent-framework-core: 1.13.0
Python Version
No response
Additional Context
5. Suggested fix
Make the condition express the comment: emit when the predictive handler actually applied pending updates, or when the tool result carried a deterministic
state_update.PredictiveStateHandler.apply_pending_updatesalready knows — having it report whether it applied anything (or checkingpending_state_updatesbefore the call) is enough for the guard, and leaves the deterministicstate_updatepath untouched.That keeps every behaviour that depends on a snapshot following a real state change, and removes only the snapshots that repeat a state nothing has modified. A narrower variant — skipping the emit when the just-finished tool is not named by any entry in
predict_state_config— would fix the reported case too, but the pending-updates check also covers a predictive tool whose arguments produced no change.A related question for the same area: when the emit is warranted, the event carries the entire state rather than the keys that changed, so a large shared state is re-sent in full for a one-key update.
StateDeltaEventalready exists for that shape and the predictive path uses it while streaming arguments; using it at the tool-result boundary too would bound the payload by the size of the change instead of the size of the conversation.