Please read this first
Describe the feature
The runner-managed retry API currently has an ownership gap: applications can provide a ModelRetrySettings.policy, but that policy is not invoked when provider advice marks replay as unsafe.
This occurs when a non-streamed Runner.run() request over the Responses WebSocket transport receives one or more response events and then loses the connection before receiving a terminal event.
The observed exception is:
websockets.exceptions.ConnectionClosedError: no close frame received or sent
The WebSocket adapter correctly:
- Detects that response processing started.
- Drops the failed connection.
- Marks the failure with
replay_safety="unsafe".
However, the runner then returns RetryDecision(retry=False) before invoking the configured application retry policy:
https://github.com/openai/openai-agents-python/blob/main/src/agents/run_internal/model_retry.py#L276-L315
This is consistent with the documented safety boundary that requests are not replayed after a response event arrives:
https://openai.github.io/openai-agents-python/models/#runner-managed-retries
I agree with that behavior as the default. The SDK cannot determine whether replaying an interrupted request is acceptable for a particular application:
- For non-streamed
Runner.run(), partial model output has not been exposed to the application.
- Application-local function tools from the incomplete model response have not been executed.
- Previously completed Runner turns and local tool outputs can be preserved by retrying only the current model request.
- However, hosted tools or other provider-side work may already have occurred and could be duplicated.
Therefore, this should not become an automatic retry. The application needs a supported way to explicitly accept that risk for a narrowly defined workload, such as a read-only research or evaluation run.
Current limitation
RetryDecision already contains an internal _approves_replay field:
https://github.com/openai/openai-agents-python/blob/main/src/agents/retry.py#L115-L123
Internal provider policies can set that approval when the provider considers replay safe, but an application retry policy cannot express the equivalent application-owned decision.
The only current workaround is to wrap or subclass OpenAIResponsesWSModel and override get_retry_advice() so that an application decision is presented as provider advice:
ModelRetryAdvice(
suggested=True,
replay_safety="safe",
)
That works, but it requires low-level model/provider plumbing and assigns the decision to the wrong abstraction boundary.
Proposed API
Please expose deliberate application replay approval through the existing retry-policy API.
For example:
RetryDecision(
retry=True,
approve_unsafe_replay=True,
reason="Application approved one replay of this read-only non-streamed turn",
)
The exact field name is flexible, but it should be intentionally explicit. An ordinary boolean True or RetryDecision(retry=True) must not bypass replay protection.
The runner would evaluate the application policy for a provider-unsafe failure and proceed only when the returned decision both requests a retry and explicitly approves replay.
The policy also needs enough public, structured context to scope that approval safely. In particular, it should be possible to determine whether:
- A response event had already arrived.
- User-visible streamed output was emitted.
- The request uses
previous_response_id or conversation_id.
- Provider replay safety is safe, unsafe, or unknown.
- The failure was a response-started transport disconnect rather than another unsafe condition.
Conceptually:
def retry_policy(context):
if (
context.attempt == 1
and not context.stream
and context.replay.response_started
and context.replay.reason == "connection_closed"
and isinstance(context.error, ConnectionClosedError)
):
return RetryDecision(
retry=True,
approve_unsafe_replay=True,
reason="Approved one replay of a read-only model turn",
)
return RetryDecision(retry=False)
Required safety properties
- Preserve the current no-replay default.
- Do not let ordinary
retry=True override replay protection.
- Keep abort errors as absolute vetoes.
- Keep retries blocked after user-visible streamed output has been emitted.
- Require explicit approval for every replay-unsafe retry decision.
- Continue respecting
max_retries, backoff, cancellation, and tracing.
- Retry only the current model request so completed Runner turns and application-local tool work are not repeated.
- Use the fresh connection already created after the WebSocket adapter drops the failed socket.
- Expose stateful-request information so applications can avoid approving replays that depend on unavailable connection-local state.
Deterministic control-flow reproduction
The policy bypass can be reproduced without making an API request:
import asyncio
from agents.retry import ModelRetryAdvice, RetryDecision
from agents.run_internal.model_retry import _evaluate_retry
policy_called = False
def policy(_context):
global policy_called
policy_called = True
return RetryDecision(retry=True)
async def main():
decision = await _evaluate_retry(
error=RuntimeError("WebSocket closed after response processing started"),
attempt=1,
max_retries=1,
retry_policy=policy,
retry_backoff=None,
stream=False,
replay_unsafe_request=False,
emitted_retry_unsafe_event=False,
provider_advice=ModelRetryAdvice(
suggested=False,
replay_safety="unsafe",
),
)
print(decision.retry, policy_called)
asyncio.run(main())
Current output:
There is presently no public RetryDecision value that can produce an explicitly application-approved replay.
Environment
openai-agents: 0.19.4
openai: 2.45.0
websockets: 15.0.1
- Python:
3.14.3
- Transport: OpenAI Responses WebSocket
- Runner interface: non-streamed
Runner.run()
auto_previous_response_id=True
I observed this twice during evaluation runs and less frequently in production. Both evaluation failures occurred well before the documented 60-minute WebSocket connection limit.
Alternatives considered
Retry the entire run
This can repeat already-completed tool calls and sub-agent work. The existing runner-managed current-model retry boundary is the correct recovery scope.
Override OpenAIResponsesWSModel.get_retry_advice()
This is functional as a workaround, but requires model/provider wrapping and makes an application-owned risk decision appear to be provider-owned advice.
Automatically classify response-started disconnects as safe
This would be incorrect because hosted or provider-side work may already have occurred. Application approval must remain explicit.
Resume the interrupted response
The Responses WebSocket documentation describes reconnecting between turns but does not document resuming an interrupted in-flight WebSocket response. Cursor-based stream resumption is documented for background SSE responses, while WebSocket response.create does not use background mode:
https://developers.openai.com/api/docs/guides/websocket-mode
https://developers.openai.com/api/docs/guides/background#streaming-a-background-response
Related issues
This is related to, but distinct from:
Those issues addressed retry classification before response processing began and correctly preserved the post-response replay veto. This request preserves that veto by default while allowing a sufficiently informed application to override it explicitly and narrowly.
Please read this first
openai-agents 0.19.4and confirmed the relevant behavior remains onmain.Describe the feature
The runner-managed retry API currently has an ownership gap: applications can provide a
ModelRetrySettings.policy, but that policy is not invoked when provider advice marks replay as unsafe.This occurs when a non-streamed
Runner.run()request over the Responses WebSocket transport receives one or more response events and then loses the connection before receiving a terminal event.The observed exception is:
The WebSocket adapter correctly:
replay_safety="unsafe".However, the runner then returns
RetryDecision(retry=False)before invoking the configured application retry policy:https://github.com/openai/openai-agents-python/blob/main/src/agents/run_internal/model_retry.py#L276-L315
This is consistent with the documented safety boundary that requests are not replayed after a response event arrives:
https://openai.github.io/openai-agents-python/models/#runner-managed-retries
I agree with that behavior as the default. The SDK cannot determine whether replaying an interrupted request is acceptable for a particular application:
Runner.run(), partial model output has not been exposed to the application.Therefore, this should not become an automatic retry. The application needs a supported way to explicitly accept that risk for a narrowly defined workload, such as a read-only research or evaluation run.
Current limitation
RetryDecisionalready contains an internal_approves_replayfield:https://github.com/openai/openai-agents-python/blob/main/src/agents/retry.py#L115-L123
Internal provider policies can set that approval when the provider considers replay safe, but an application retry policy cannot express the equivalent application-owned decision.
The only current workaround is to wrap or subclass
OpenAIResponsesWSModeland overrideget_retry_advice()so that an application decision is presented as provider advice:That works, but it requires low-level model/provider plumbing and assigns the decision to the wrong abstraction boundary.
Proposed API
Please expose deliberate application replay approval through the existing retry-policy API.
For example:
The exact field name is flexible, but it should be intentionally explicit. An ordinary boolean
TrueorRetryDecision(retry=True)must not bypass replay protection.The runner would evaluate the application policy for a provider-unsafe failure and proceed only when the returned decision both requests a retry and explicitly approves replay.
The policy also needs enough public, structured context to scope that approval safely. In particular, it should be possible to determine whether:
previous_response_idorconversation_id.Conceptually:
Required safety properties
retry=Trueoverride replay protection.max_retries, backoff, cancellation, and tracing.Deterministic control-flow reproduction
The policy bypass can be reproduced without making an API request:
Current output:
There is presently no public
RetryDecisionvalue that can produce an explicitly application-approved replay.Environment
openai-agents:0.19.4openai:2.45.0websockets:15.0.13.14.3Runner.run()auto_previous_response_id=TrueI observed this twice during evaluation runs and less frequently in production. Both evaluation failures occurred well before the documented 60-minute WebSocket connection limit.
Alternatives considered
Retry the entire run
This can repeat already-completed tool calls and sub-agent work. The existing runner-managed current-model retry boundary is the correct recovery scope.
Override
OpenAIResponsesWSModel.get_retry_advice()This is functional as a workaround, but requires model/provider wrapping and makes an application-owned risk decision appear to be provider-owned advice.
Automatically classify response-started disconnects as safe
This would be incorrect because hosted or provider-side work may already have occurred. Application approval must remain explicit.
Resume the interrupted response
The Responses WebSocket documentation describes reconnecting between turns but does not document resuming an interrupted in-flight WebSocket response. Cursor-based stream resumption is documented for background SSE responses, while WebSocket
response.createdoes not use background mode:https://developers.openai.com/api/docs/guides/websocket-mode
https://developers.openai.com/api/docs/guides/background#streaming-a-background-response
Related issues
This is related to, but distinct from:
server_errorframes bypass model retry policies #3990Those issues addressed retry classification before response processing began and correctly preserved the post-response replay veto. This request preserves that veto by default while allowing a sufficiently informed application to override it explicitly and narrowly.