-
Notifications
You must be signed in to change notification settings - Fork 4.2k
fix: complete realtime tool failures #3530
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,7 +22,13 @@ | |
| from ..logger import logger | ||
| from ..run_config import ToolErrorFormatterArgs | ||
| from ..run_context import RunContextWrapper, TContext | ||
| from ..tool import DEFAULT_APPROVAL_REJECTION_MESSAGE, FunctionTool, invoke_function_tool | ||
| from ..tool import ( | ||
| DEFAULT_APPROVAL_REJECTION_MESSAGE, | ||
| FunctionTool, | ||
| default_tool_error_function, | ||
| invoke_function_tool, | ||
| maybe_invoke_function_tool_failure_error_function, | ||
| ) | ||
| from ..tool_context import ToolContext | ||
| from ..util._approvals import evaluate_needs_approval_setting | ||
| from .agent import RealtimeAgent | ||
|
|
@@ -114,9 +120,15 @@ class _PendingToolOutput: | |
|
|
||
|
|
||
| class _PendingToolOutputSendError(RuntimeError): | ||
| def __init__(self, call_id: str, cause: BaseException) -> None: | ||
| def __init__( | ||
| self, | ||
| call_id: str, | ||
| cause: BaseException, | ||
| original_error: BaseException | None = None, | ||
| ) -> None: | ||
| super().__init__(str(cause)) | ||
| self.call_id = call_id | ||
| self.original_error = original_error | ||
|
|
||
|
|
||
| class RealtimeSession(RealtimeModelListener): | ||
|
|
@@ -714,6 +726,62 @@ async def reject_tool_call( | |
| finally: | ||
| self._finish_tool_call(call_id, mark_completed=mark_completed) | ||
|
|
||
| async def _send_function_tool_failure_output( | ||
| self, | ||
| event: RealtimeModelToolCallEvent, | ||
| *, | ||
| tool: FunctionTool, | ||
| tool_context: ToolContext[Any], | ||
| agent: RealtimeAgent, | ||
| error: Exception, | ||
| ) -> bool: | ||
| output = await maybe_invoke_function_tool_failure_error_function( | ||
| function_tool=tool, | ||
| context=tool_context, | ||
| error=error, | ||
| ) | ||
| if output is None: | ||
| return False | ||
|
|
||
| try: | ||
| await self._send_tool_output_completion( | ||
| _PendingToolOutput( | ||
| tool_call=event, | ||
| output=output, | ||
| start_response=True, | ||
| tool_end_event=RealtimeToolEnd( | ||
| info=self._event_info, | ||
| tool=tool, | ||
| output=output, | ||
| agent=agent, | ||
| arguments=event.arguments, | ||
| ), | ||
| ) | ||
| ) | ||
| except _PendingToolOutputSendError as send_error: | ||
| send_error.original_error = error | ||
| raise | ||
| return True | ||
|
|
||
| async def _send_handoff_failure_output( | ||
| self, | ||
| event: RealtimeModelToolCallEvent, | ||
| *, | ||
| tool_context: ToolContext[Any], | ||
| error: Exception, | ||
| ) -> None: | ||
| try: | ||
| await self._send_tool_output_completion( | ||
| _PendingToolOutput( | ||
| tool_call=event, | ||
| output=default_tool_error_function(tool_context, error), | ||
| start_response=True, | ||
| ) | ||
| ) | ||
| except _PendingToolOutputSendError as send_error: | ||
| send_error.original_error = error | ||
| raise | ||
|
|
||
| async def _handle_tool_call( | ||
| self, | ||
| event: RealtimeModelToolCallEvent, | ||
|
|
@@ -773,11 +841,22 @@ async def _handle_tool_call( | |
| tool_arguments=event.arguments, | ||
| agent=agent, | ||
| ) | ||
| result = await invoke_function_tool( | ||
| function_tool=func_tool, | ||
| context=tool_context, | ||
| arguments=event.arguments, | ||
| ) | ||
| try: | ||
| result = await invoke_function_tool( | ||
| function_tool=func_tool, | ||
| context=tool_context, | ||
| arguments=event.arguments, | ||
| ) | ||
| except Exception as exc: | ||
| if await self._send_function_tool_failure_output( | ||
| event, | ||
| tool=func_tool, | ||
| tool_context=tool_context, | ||
| agent=agent, | ||
| error=exc, | ||
| ): | ||
| mark_completed = True | ||
|
Comment on lines
+851
to
+858
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the model send for this synthesized failure output fails, Useful? React with 👍 / 👎. |
||
| raise | ||
|
|
||
| await self._send_tool_output_completion( | ||
| _PendingToolOutput( | ||
|
|
@@ -806,11 +885,20 @@ async def _handle_tool_call( | |
| ) | ||
|
|
||
| # Execute the handoff to get the new agent | ||
| result = await handoff.on_invoke_handoff(self._context_wrapper, event.arguments) | ||
| if not isinstance(result, RealtimeAgent): | ||
| raise UserError( | ||
| f"Handoff {handoff.tool_name} returned invalid result: {type(result)}" | ||
| try: | ||
| result = await handoff.on_invoke_handoff(self._context_wrapper, event.arguments) | ||
| if not isinstance(result, RealtimeAgent): | ||
| raise UserError( | ||
| f"Handoff {handoff.tool_name} returned invalid result: {type(result)}" | ||
| ) | ||
| except Exception as exc: | ||
| await self._send_handoff_failure_output( | ||
| event, | ||
| tool_context=tool_context, | ||
| error=exc, | ||
| ) | ||
|
Comment on lines
+895
to
899
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For handoff failures, if Useful? React with 👍 / 👎. |
||
| mark_completed = True | ||
| raise | ||
|
|
||
| # Store previous agent for event | ||
| previous_agent = agent | ||
|
|
@@ -1203,6 +1291,19 @@ def _on_tool_call_task_done(self, task: asyncio.Task[Any]) -> None: | |
| ) | ||
| ) | ||
| ) | ||
| original_error = exception.original_error | ||
| if original_error is not None: | ||
| logger.exception("Realtime tool call task failed", exc_info=original_error) | ||
| if self._stored_exception is None: | ||
| self._stored_exception = original_error | ||
| asyncio.create_task( | ||
| self._put_event( | ||
| RealtimeError( | ||
| info=self._event_info, | ||
| error={"message": f"Tool call task failed: {original_error}"}, | ||
| ) | ||
| ) | ||
| ) | ||
| return | ||
|
|
||
| logger.exception("Realtime tool call task failed", exc_info=exception) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
async_tool_callsis disabled (or approval calls await_handle_tool_calldirectly), a transientsend_eventfailure while reporting a tool exception makes this helper re-raise_PendingToolOutputSendError; since_on_tool_call_task_doneis not involved,original_erroris never consumed and the app seessend failedinstead of the original tool error. Fresh evidence beyond the existing async comments:original_erroris only read in_on_tool_call_task_done, which the synchronous path bypasses.Useful? React with 👍 / 👎.