Repository navigation
Python: Don't emit response.completed after a cancelled Foundry hosting run - #8565
Conversation
…ng run Fixes microsoft#8564. When cancellation_signal fires, _handle_inner_workflow and _handle_inner_agent stop their _SignalledIterator loop and return normally instead of raising, so the post-loop code in _handle_prepared_response treated the cancelled run as a success and still emitted response.completed (or response.incomplete). Add an early return once the inner generator drains so a cancelled run emits no terminal event, letting the cancel-aware host layer synthesize the cancelled terminal instead.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Cancellation during tracker-closing events can still produce an incorrect terminal event.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
What changed in this PR
Prevents cancelled Foundry-hosted Python runs from emitting successful terminal events.
Changes:
- Stops terminal emission when cancellation is detected.
- Updates agent and workflow cancellation tests.
| File | Description |
|---|---|
_responses.py |
Adds cancellation-aware terminal suppression. |
test_responses.py |
Updates cancellation expectations and coverage. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
…rminal event Copilot review on microsoft#8565 pointed out that the cancellation_signal check added there runs before tracker.close(), but each event that loop yields suspends the handler. A cancellation arriving while those trailing *.done events are being consumed still fell through to emit_completed/emit_incomplete, reproducing the microsoft#8564 race for a narrower window. Recheck the signal once more immediately after draining tracker.close(), before selecting the terminal event.
Only skip the response.completed/incomplete terminal when the cancellation_signal cause is an explicit /cancel (or non-bg disconnect), i.e. context.client_cancelled is True. cancellation_signal also fires for steering pressure with no cause flag set, so without this gate a steered turn's partial output would be silently dropped instead of completing normally. Update the existing explicit-cancel tests to set client_cancelled alongside the signal, and add a regression test for the steering case.
|
leilei3167, thanks for the update. The current head now conflicts with |
…_reason Resolve conflict in foundry_hosting _responses: retain client_cancelled recheck after tracker.close(), and adopt main's incomplete_reason path for emit_incomplete so the branch can re-enter the merge queue.
|
Thanks for the heads-up — merged latest |
…ng run (microsoft#8565) * Python: Don't emit response.completed after a cancelled Foundry hosting run Fixes microsoft#8564. When cancellation_signal fires, _handle_inner_workflow and _handle_inner_agent stop their _SignalledIterator loop and return normally instead of raising, so the post-loop code in _handle_prepared_response treated the cancelled run as a success and still emitted response.completed (or response.incomplete). Add an early return once the inner generator drains so a cancelled run emits no terminal event, letting the cancel-aware host layer synthesize the cancelled terminal instead. * Python: Recheck cancellation after draining tracker.close() before terminal event Copilot review on microsoft#8565 pointed out that the cancellation_signal check added there runs before tracker.close(), but each event that loop yields suspends the handler. A cancellation arriving while those trailing *.done events are being consumed still fell through to emit_completed/emit_incomplete, reproducing the microsoft#8564 race for a narrower window. Recheck the signal once more immediately after draining tracker.close(), before selecting the terminal event. * Python: Gate cancellation-terminal suppression on client_cancelled Only skip the response.completed/incomplete terminal when the cancellation_signal cause is an explicit /cancel (or non-bg disconnect), i.e. context.client_cancelled is True. cancellation_signal also fires for steering pressure with no cause flag set, so without this gate a steered turn's partial output would be silently dropped instead of completing normally. Update the existing explicit-cancel tests to set client_cancelled alongside the signal, and add a regression test for the steering case.

Motivation & Context
After a Foundry-hosted response is cancelled through
cancellation_signal,_handle_inner_workflowand_handle_inner_agentstop their_SignalledIteratorloop and return normally instead of raising. The post-loop code in_handle_prepared_responsetreats that normal return as a success and still yieldsresponse.completed(orresponse.incomplete).azure-ai-agentserver-responseshappens to rewrite the terminal tocancelledon the wire, but anything sitting between the hosting handler and that layer (for example a subclass wrapping_handle_responseto persist results) sees a successful terminal for a run that was actually cancelled.Description & Review Guide
_handle_prepared_response, return without yielding a terminal event oncecancellation_signalis observed set after the inner generator drains, instead of falling through totracker.close()/emit_completed/emit_incomplete.response.completed; callers consuming the handler stream can rely on the absence of a terminal event (or the cancel-aware host layer's synthesizedcancelledterminal) to distinguish a cancelled run from a finished one.tracker.close()on the cancelled path is safe (the underlying_SignalledIterator/ResponseStreamalready close the wrapped iterator in their ownfinallyblocks) and that this doesn't affect the shutdown/recovery path, which is gated separately.Related Issue
Fixes #8564
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.