Skip to content

fix(run): cancel the parallel input-guardrail task when the model turn fails - #3982

Merged
seratch merged 1 commit into
openai:mainfrom
gunjanjaswal:fix/parallel-input-guardrail-task-leak
Jul 27, 2026
Merged

fix(run): cancel the parallel input-guardrail task when the model turn fails#3982
seratch merged 1 commit into
openai:mainfrom
gunjanjaswal:fix/parallel-input-guardrail-task-leak

Conversation

@gunjanjaswal

Copy link
Copy Markdown
Contributor

In the parallel input-guardrail path (AgentRunner.run), the guardrail coroutine and the model turn are awaited together with asyncio.gather without return_exceptions=True, and the only handler catches InputGuardrailTripwireTriggered.

If the model turn raises any other exception (a ModelBehaviorError, a provider RuntimeError) before the guardrail finishes, gather propagates the model error but leaves the sibling guardrail task running. It keeps making network calls and side effects after the run has already failed and returned, and an error raised inside that guardrail after gather resolves is silently swallowed. Guardrails default to run_in_parallel=True, so this is the common path whenever an input guardrail exists.

The fix wraps the guardrail coroutine in an explicit task and adds a symmetric cleanup branch: on any non-tripwire failure, cancel whichever of the guardrail and model tasks is still pending and drain both with return_exceptions=True before re-raising. The existing tripwire behavior and the flag-gated model-task cancellation are preserved, and the streamed path is unchanged.

This complements #3239, which cancelled sibling guardrails inside run_input_guardrails itself. That fix does not reach the orchestration-level gather in run.py, so when the model task is the one that raises, the guardrail task is still orphaned. This closes that gap.

Tests cover a model error cancelling the in-flight guardrail task, and a guardrail's non-tripwire error propagating and cancelling the model task; the existing tripwire tests still pass. Both new tests fail on main and pass with this change. Full suite: 5783 passed, 7 skipped; ruff, format, and mypy clean.

…n fails

In the parallel input-guardrail path, the guardrail coroutine and the model
turn are awaited together with asyncio.gather without return_exceptions=True,
and the only handler catches InputGuardrailTripwireTriggered. When the model
turn (or a guardrail) raises any other exception, gather propagates it but does
not cancel the sibling awaitable, so it is orphaned and keeps running network
calls and side effects after the run has already failed and returned. An
exception raised inside the guardrail task after gather resolves is also
swallowed.

Wrap the guardrail coroutine in an explicit task and add a symmetric cleanup
branch: on any non-tripwire failure, cancel whichever of the guardrail/model
tasks is still pending and drain both with return_exceptions=True before
re-raising. The existing tripwire behavior and the flag-gated model-task
cancellation are preserved.
@seratch seratch added this to the 0.19.x milestone Jul 27, 2026
@seratch
seratch merged commit 5804bd0 into openai:main Jul 27, 2026
9 checks passed
@gunjanjaswal

Copy link
Copy Markdown
Contributor Author

Thanks for the review and merge, @seratch. Good to have the parallel path cancel and drain the guardrail task instead of leaving it running after the turn fails.

@gunjanjaswal
gunjanjaswal deleted the fix/parallel-input-guardrail-task-leak branch July 28, 2026 02:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants