Python: [Bug]: Functional workflow drops the response to a `request_info` and asks it again when a sibling branch pauses first · Issue #9541 · microsoft/agent-framework · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Python: [Bug]: Functional workflow drops the response to a request_info and asks it again when a sibling branch pauses first #9541
A functional workflow runs two branches with asyncio.gather. The left branch awaits a step and then calls request_info("approve"); the right branch calls request_info("review"). Each pending request is answered once, and the run is resumed with exactly the responses to the requests it reported.
The first run pauses on review. The second run receives the response to review, but the left branch, whose step is now served from the cache, reaches request_info("approve") and pauses first. The step that asks review is entered after this pause and stops at entry, before it looks up its response. The response to review is never consumed, and the third run, which carries only the response to approve, asks review again.
A client that answers each request once therefore sees the same request twice.
Expected Behavior
A response supplied for a pending request is delivered to the step that asked it, as in 1.20.0 and 1.21.0: the second run consumes the response to review and pauses on approve, and the third run completes without asking review again.
Steps to Reproduce
Save the script below and run it with agent-framework-core 1.22.0. No model or checkpoint storage is needed.
The first run reports the pending request review; answer it.
The second run reports approve; answer it.
The third run reports review again.
Minimal Reproduction
importasyncioimportcollectionsfromimportlib.metadataimportversionfromagent_frameworkimportRunContext, step, workflow@stepasyncdeffetch(name: str) ->int:
awaitasyncio.sleep(0.01) # e.g. a model or HTTP callreturn1@stepasyncdefask(name: str, ctx: RunContext) ->int:
returnawaitctx.request_info(f"{name}?", int, request_id=name)
asyncdefleft() ->int:
returnawaitfetch("data") +awaitask("approve")
asyncdefright() ->int:
returnawaitask("review")
@workflowasyncdefflow(x: int) ->int:
a, b=awaitasyncio.gather(left(), right())
returna+basyncdefmain():
app=flow.build()
result=awaitapp.run(0)
asked=collections.Counter()
whilepending:=result.get_request_info_events():
ids= [event.request_idforeventinpending]
print("asks:", ids)
asked.update(ids)
result=awaitapp.run(responses={rid: 1forridinids})
print(f"agent-framework-core {version('agent-framework-core')}: outputs={result.get_outputs()}, asked={dict(asked)}")
assertall(count==1forcountinasked.values()), "an answered request was asked again"asyncio.run(main())
Error Messages and Stack Traces
asks: ['review']
asks: ['approve']
asks: ['review']
agent-framework-core 1.22.0: outputs=[3], asked={'review': 2, 'approve': 1}
AssertionError: an answered request was asked again
On 1.20.0 and 1.21.0 the same script prints:
asks: ['review']
asks: ['approve']
agent-framework-core 1.21.0: outputs=[3], asked={'review': 1, 'approve': 1}
Package Versions
agent-framework-core: 1.22.0 (also main at ebcf4db)
Python Version
Python 3.13.9
Operating System
macOS
Regression
Yes
Additional Context
Since 1.22.0, StepWrapper.__call__ first calls ctx._raise_if_current_task_interrupted(entering_step=True). Once any branch has paused (ctx._workflow_interruption is set), a step entered afterwards raises the run's interruption before it looks up its cached result or its response (ctx._get_response).
If the request that step asks was answered in this run, the response is lost: _set_responses replaces ctx._responses on every run, and the next run is resumed with the responses to the requests it reported, which no longer include the answered one. The request is then asked again.
Whether the step is entered before or after the sibling's pause depends on scheduling. Any await before a branch's request changes the interleaving; checkpoint storage with asynchronous writes (FileCheckpointStorage) is one such source. With it, two branches that each ask three requests in turn, with no other await, ask the first request of the second branch three times; with InMemoryCheckpointStorage they ask each request once.
Possible directions: let a step whose request already has a response in this run proceed to consume it, or keep the responses of a run that were not consumed for the next resumption.
🤖 Automated triage reproduction notes(agent-authored — trust but verify)
Agent analysis
Repro: python/packages/core/agent_framework/_workflows/_functional.py::StepWrapper.__call__ at line 1515 stops an answered request’s step at entry when a concurrent sibling has already paused. Minimal repro: gather a delayed cached-step branch requesting approve with a branch requesting review, then resume each reported request once; the sequence becomes review, approve, review.
Activity
github-actions commentedon Oct 19, 2026
🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
Repro:
python/packages/core/agent_framework/_workflows/_functional.py::StepWrapper.__call__at line 1515 stops an answered request’s step at entry when a concurrent sibling has already paused. Minimal repro: gather a delayed cached-step branch requestingapprovewith a branch requestingreview, then resume each reported request once; the sequence becomesreview,approve,review.python/packages/core/tests/workflow/test_functional_workflow.py1.22.01.22.0