From bf21fe0dd52cde84fff516595e947cab529ca35d Mon Sep 17 00:00:00 2001 From: c <37263590+Aphroq@users.noreply.github.com> Date: Fri, 1 May 2026 16:51:45 +0000 Subject: [PATCH 1/2] test: cover realtime tool timeout behaviors in realtime session Add focused realtime session tests for tool timeout branches. - cover direct _handle_tool_call() behavior for timeout_behavior="raise_exception" - cover async timeout_error_function output handling - cover async on_event() failure propagation into RealtimeError --- tests/realtime/test_session.py | 135 ++++++++++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 1 deletion(-) diff --git a/tests/realtime/test_session.py b/tests/realtime/test_session.py index c1c919a866..e10c62fe5d 100644 --- a/tests/realtime/test_session.py +++ b/tests/realtime/test_session.py @@ -8,7 +8,7 @@ import pytest from pydantic import BaseModel, ConfigDict -from agents.exceptions import UserError +from agents.exceptions import ToolTimeoutError, UserError from agents.guardrail import GuardrailFunctionOutput, OutputGuardrail from agents.handoffs import Handoff from agents.realtime.agent import RealtimeAgent @@ -1058,6 +1058,139 @@ async def invoke_slow_tool(_ctx: ToolContext[Any], _arguments: str) -> str: assert start_response is True assert "timed out" in sent_output.lower() + @pytest.mark.asyncio + async def test_function_tool_timeout_raise_exception_propagates(self, mock_model, mock_agent): + async def invoke_slow_tool(_ctx: ToolContext[Any], _arguments: str) -> str: + await asyncio.sleep(0.2) + return "done" + + timeout_tool = FunctionTool( + name="slow_tool", + description="slow", + params_json_schema={"type": "object", "properties": {}}, + on_invoke_tool=invoke_slow_tool, + timeout_seconds=0.01, + timeout_behavior="raise_exception", + ) + mock_agent.get_all_tools.return_value = [timeout_tool] + + session = RealtimeSession(mock_model, mock_agent, None) + tool_call_event = RealtimeModelToolCallEvent( + name="slow_tool", + call_id="call_timeout_raise", + arguments="{}", + ) + + with pytest.raises(ToolTimeoutError, match="timed out"): + await session._handle_tool_call(tool_call_event) + + assert len(mock_model.sent_tool_outputs) == 0 + assert session._event_queue.qsize() == 1 + + tool_start_event = await session._event_queue.get() + assert isinstance(tool_start_event, RealtimeToolStart) + assert tool_start_event.tool == timeout_tool + assert tool_start_event.arguments == "{}" + + @pytest.mark.asyncio + async def test_function_tool_timeout_uses_async_error_function_result( + self, mock_model, mock_agent + ): + async def invoke_slow_tool(_ctx: ToolContext[Any], _arguments: str) -> str: + await asyncio.sleep(0.2) + return "done" + + async def format_timeout_error(ctx: ToolContext[Any], error: Exception) -> str: + assert isinstance(error, ToolTimeoutError) + assert ctx.tool_name == "slow_tool" + assert ctx.tool_call_id == "call_timeout_custom" + return f"async-timeout:{error.tool_name}:{error.timeout_seconds:g}" + + timeout_tool = FunctionTool( + name="slow_tool", + description="slow", + params_json_schema={"type": "object", "properties": {}}, + on_invoke_tool=invoke_slow_tool, + timeout_seconds=0.01, + timeout_error_function=format_timeout_error, + ) + mock_agent.get_all_tools.return_value = [timeout_tool] + + session = RealtimeSession(mock_model, mock_agent, None) + tool_call_event = RealtimeModelToolCallEvent( + name="slow_tool", + call_id="call_timeout_custom", + arguments="{}", + ) + + await session._handle_tool_call(tool_call_event) + + assert len(mock_model.sent_tool_outputs) == 1 + sent_call, sent_output, start_response = mock_model.sent_tool_outputs[0] + assert sent_call == tool_call_event + assert sent_output == "async-timeout:slow_tool:0.01" + assert start_response is True + + assert session._event_queue.qsize() == 2 + await session._event_queue.get() + tool_end_event = await session._event_queue.get() + assert isinstance(tool_end_event, RealtimeToolEnd) + assert tool_end_event.output == "async-timeout:slow_tool:0.01" + + @pytest.mark.asyncio + async def test_function_call_event_timeout_raise_exception_enqueues_error( + self, mock_model, mock_agent + ): + async def invoke_slow_tool(_ctx: ToolContext[Any], _arguments: str) -> str: + await asyncio.sleep(0.2) + return "done" + + timeout_tool = FunctionTool( + name="slow_tool", + description="slow", + params_json_schema={"type": "object", "properties": {}}, + on_invoke_tool=invoke_slow_tool, + timeout_seconds=0.01, + timeout_behavior="raise_exception", + ) + mock_agent.get_all_tools.return_value = [timeout_tool] + + session = RealtimeSession(mock_model, mock_agent, None) + tool_call_event = RealtimeModelToolCallEvent( + name="slow_tool", + call_id="call_timeout_async", + arguments="{}", + ) + + await session.on_event(tool_call_event) + + tool_call_tasks = list(session._tool_call_tasks) + assert len(tool_call_tasks) == 1 + await asyncio.gather(*tool_call_tasks, return_exceptions=True) + + for _ in range(10): + if session._event_queue.qsize() >= 3: + break + await asyncio.sleep(0.01) + + assert isinstance(session._stored_exception, ToolTimeoutError) + assert session._stored_exception.tool_name == "slow_tool" + assert len(mock_model.sent_tool_outputs) == 0 + + events = [] + while not session._event_queue.empty(): + events.append(await session._event_queue.get()) + + assert any( + isinstance(event, RealtimeRawModelEvent) and event.data == tool_call_event + for event in events + ) + assert any(isinstance(event, RealtimeToolStart) for event in events) + + error_event = next(event for event in events if isinstance(event, RealtimeError)) + assert "Tool call task failed" in error_event.error["message"] + assert "timed out" in error_event.error["message"] + @pytest.mark.asyncio async def test_function_tool_with_multiple_tools_available(self, mock_model, mock_agent): """Test function tool execution when multiple tools are available""" From 82eb4b64a808096b9f41859c6966f5a74eb38e54 Mon Sep 17 00:00:00 2001 From: c <37263590+Aphroq@users.noreply.github.com> Date: Sat, 2 May 2026 11:54:06 +0000 Subject: [PATCH 2/2] test: address realtime timeout coverage review --- tests/realtime/test_session.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/realtime/test_session.py b/tests/realtime/test_session.py index e10c62fe5d..6abfcd41a9 100644 --- a/tests/realtime/test_session.py +++ b/tests/realtime/test_session.py @@ -60,6 +60,7 @@ RealtimeModelSendUserInput, ) from agents.realtime.session import REJECTION_MESSAGE, RealtimeSession, _serialize_tool_output +from agents.run_context import RunContextWrapper from agents.tool import FunctionTool from agents.tool_context import ToolContext @@ -1100,8 +1101,9 @@ async def invoke_slow_tool(_ctx: ToolContext[Any], _arguments: str) -> str: await asyncio.sleep(0.2) return "done" - async def format_timeout_error(ctx: ToolContext[Any], error: Exception) -> str: + async def format_timeout_error(ctx: RunContextWrapper[Any], error: Exception) -> str: assert isinstance(error, ToolTimeoutError) + assert isinstance(ctx, ToolContext) assert ctx.tool_name == "slow_tool" assert ctx.tool_call_id == "call_timeout_custom" return f"async-timeout:{error.tool_name}:{error.timeout_seconds:g}" @@ -1168,18 +1170,16 @@ async def invoke_slow_tool(_ctx: ToolContext[Any], _arguments: str) -> str: assert len(tool_call_tasks) == 1 await asyncio.gather(*tool_call_tasks, return_exceptions=True) - for _ in range(10): - if session._event_queue.qsize() >= 3: - break - await asyncio.sleep(0.01) - assert isinstance(session._stored_exception, ToolTimeoutError) assert session._stored_exception.tool_name == "slow_tool" assert len(mock_model.sent_tool_outputs) == 0 events = [] - while not session._event_queue.empty(): - events.append(await session._event_queue.get()) + while True: + event = await asyncio.wait_for(session._event_queue.get(), timeout=1) + events.append(event) + if isinstance(event, RealtimeError): + break assert any( isinstance(event, RealtimeRawModelEvent) and event.data == tool_call_event