fix: send tool output to model when function tool raises exception#3373
fix: send tool output to model when function tool raises exception#3373bbiiggjjuu wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR ensures realtime sessions don’t leave the model waiting indefinitely when a known function tool call (or a handoff tool call) raises an exception. It mirrors the “unknown tool” handling added in #3287 by sending a RealtimeModelSendToolOutput(start_response=True) to the model before re-raising the exception.
Changes:
- Wrap known function tool invocation in a
try/exceptand send model-visible error tool output before re-raising. - Apply the same protection to the handoff invocation path.
- Update session tests to expect a tool output to be sent when timeouts/exceptions are raised.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
src/agents/realtime/session.py |
Sends a model-visible tool output when a known tool/handoff raises, before re-raising. |
tests/realtime/test_session.py |
Updates expectations to assert a tool output is sent for raised timeouts/exceptions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| except Exception: | ||
| await self._model.send_event( | ||
| RealtimeModelSendToolOutput( | ||
| tool_call=event, | ||
| output=f"Tool '{event.name}' failed", |
| except Exception: | ||
| await self._model.send_event( | ||
| RealtimeModelSendToolOutput( | ||
| tool_call=event, | ||
| output=f"Handoff '{event.name}' failed", |
| await session._handle_tool_call(tool_call_event) | ||
|
|
||
| assert len(mock_model.sent_tool_outputs) == 0 | ||
| assert len(mock_model.sent_tool_outputs) == 1 |
| assert isinstance(session._stored_exception, ToolTimeoutError) | ||
| assert session._stored_exception.tool_name == "slow_tool" | ||
| assert len(mock_model.sent_tool_outputs) == 0 | ||
| assert len(mock_model.sent_tool_outputs) == 1 |
| # But no tool output should have been sent and no end event queued | ||
| assert len(mock_model.sent_tool_outputs) == 0 | ||
| # An error tool output should be sent to the model before re-raising | ||
| assert len(mock_model.sent_tool_outputs) == 1 |
|
Nice fix. The subtle part here is that raising locally and returning a tool result to the model are two different contracts. For realtime sessions, leaving the model waiting on a |
Summary
When a known function tool invocation raises an exception (e.g.
ToolTimeoutErrorwith
timeout_behavior="raise_exception", or user-raisedValueError), the sessionnow sends
RealtimeModelSendToolOutputwith an error message andstart_response=Trueback to the model before re-raising the exception.
This mirrors the behavior introduced for unknown tool calls in #3287, and ensures
the model is not left waiting for a function call result that will never arrive.
The same protection is also applied to the handoff branch where
handoff.on_invoke_handoffmay raise.Tests
make format,make lint,'make tests' passmypyandpyrightpass on changed filesuv run pytest tests/realtime/test_session.py -k "tool_timeout or tool_exception" -v— 5/5 passmake tests— 4524 passed.The single failure (
tests/test_trace_processor.py::test_tracing_atexit_cleanup_timeout_preserves_process_exit_code_on_504)is a pre-existing environment-specific test unrelated to this change: it spawns a child process that exercises
httpxretry behavior during trace export, and the child process timed out undersubprocess.run(timeout=3.0)in this environment.It belongs to the tracing subsystem (
tests/test_trace_processor.py) and is not affected by changes tosrc/agents/realtime/session.pyortests/realtime/test_session.py,and even if I haven’t modified the code, this test doesn’t run on my local environment.Issue number
Closes #3356
Checks
make lintandmake format