From c1e43a62812f2492225b98772124db7aaf884d1d Mon Sep 17 00:00:00 2001 From: Daniele Morotti <58258368+DanieleMorotti@users.noreply.github.com> Date: Thu, 22 May 2025 18:28:05 +0200 Subject: [PATCH 1/4] Added RunErrorDetails object for MaxTurnsExceeded exception --- src/agents/__init__.py | 3 ++- src/agents/exceptions.py | 7 +++++-- src/agents/result.py | 18 +++++++++++++++++- src/agents/run.py | 17 +++++++++++++++-- src/agents/util/_pretty_print.py | 11 ++++++++++- 5 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/agents/__init__.py b/src/agents/__init__.py index 58949157a..f36cefc69 100644 --- a/src/agents/__init__.py +++ b/src/agents/__init__.py @@ -44,7 +44,7 @@ from .models.openai_chatcompletions import OpenAIChatCompletionsModel from .models.openai_provider import OpenAIProvider from .models.openai_responses import OpenAIResponsesModel -from .result import RunResult, RunResultStreaming +from .result import RunErrorDetails, RunResult, RunResultStreaming from .run import RunConfig, Runner from .run_context import RunContextWrapper, TContext from .stream_events import ( @@ -204,6 +204,7 @@ def enable_verbose_stdout_logging(): "AgentHooks", "RunContextWrapper", "TContext", + "RunErrorDetails", "RunResult", "RunResultStreaming", "RunConfig", diff --git a/src/agents/exceptions.py b/src/agents/exceptions.py index 78898f017..93cd22aaa 100644 --- a/src/agents/exceptions.py +++ b/src/agents/exceptions.py @@ -1,7 +1,8 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from .guardrail import InputGuardrailResult, OutputGuardrailResult + from .result import RunErrorDetails class AgentsException(Exception): @@ -12,9 +13,11 @@ class MaxTurnsExceeded(AgentsException): """Exception raised when the maximum number of turns is exceeded.""" message: str + run_error_details: Optional["RunErrorDetails"] - def __init__(self, message: str): + def __init__(self, message: str, run_error_details: Optional["RunErrorDetails"] = None): self.message = message + self.run_error_details = run_error_details class ModelBehaviorError(AgentsException): diff --git a/src/agents/result.py b/src/agents/result.py index 243db155c..cfab4dfce 100644 --- a/src/agents/result.py +++ b/src/agents/result.py @@ -18,7 +18,11 @@ from .run_context import RunContextWrapper from .stream_events import StreamEvent from .tracing import Trace -from .util._pretty_print import pretty_print_result, pretty_print_run_result_streaming +from .util._pretty_print import ( + pretty_print_result, + pretty_print_run_error_details, + pretty_print_run_result_streaming, +) if TYPE_CHECKING: from ._run_impl import QueueCompleteSentinel @@ -244,3 +248,15 @@ def _cleanup_tasks(self): def __str__(self) -> str: return pretty_print_run_result_streaming(self) + +@dataclass +class RunErrorDetails(RunResultBase): + _last_agent: Agent[Any] + + @property + def last_agent(self) -> Agent[Any]: + """The last agent that was run.""" + return self._last_agent + + def __str__(self) -> str: + return pretty_print_run_error_details(self) diff --git a/src/agents/run.py b/src/agents/run.py index b196c3bf1..6b2ac7a74 100644 --- a/src/agents/run.py +++ b/src/agents/run.py @@ -35,7 +35,7 @@ from .model_settings import ModelSettings from .models.interface import Model, ModelProvider from .models.multi_provider import MultiProvider -from .result import RunResult, RunResultStreaming +from .result import RunErrorDetails, RunResult, RunResultStreaming from .run_context import RunContextWrapper, TContext from .stream_events import AgentUpdatedStreamEvent, RawResponsesStreamEvent from .tool import Tool @@ -208,7 +208,20 @@ async def run( data={"max_turns": max_turns}, ), ) - raise MaxTurnsExceeded(f"Max turns ({max_turns}) exceeded") + run_error_details = RunErrorDetails( + input=original_input, + new_items=generated_items, + raw_responses=model_responses, + final_output=None, + input_guardrail_results=input_guardrail_results, + output_guardrail_results=[], + context_wrapper=context_wrapper, + _last_agent=current_agent + ) + raise MaxTurnsExceeded( + f"Max turns ({max_turns}) exceeded", + run_error_details + ) logger.debug( f"Running agent {current_agent.name} (turn {current_turn})", diff --git a/src/agents/util/_pretty_print.py b/src/agents/util/_pretty_print.py index afd3e2b1b..69a79740e 100644 --- a/src/agents/util/_pretty_print.py +++ b/src/agents/util/_pretty_print.py @@ -3,7 +3,7 @@ from pydantic import BaseModel if TYPE_CHECKING: - from ..result import RunResult, RunResultBase, RunResultStreaming + from ..result import RunErrorDetails, RunResult, RunResultBase, RunResultStreaming def _indent(text: str, indent_level: int) -> str: @@ -37,6 +37,15 @@ def pretty_print_result(result: "RunResult") -> str: return output +def pretty_print_run_error_details(result: "RunErrorDetails") -> str: + output = "RunErrorDetails:" + output += f'\n- Last agent: Agent(name="{result.last_agent.name}", ...)' + output += f"\n- {len(result.new_items)} new item(s)" + output += f"\n- {len(result.raw_responses)} raw response(s)" + output += f"\n- {len(result.input_guardrail_results)} input guardrail result(s)" + output += "\n(See `RunErrorDetails` for more details)" + + return output def pretty_print_run_result_streaming(result: "RunResultStreaming") -> str: output = "RunResultStreaming:" From 387b5eba7342836327580e982935533ba2a84b15 Mon Sep 17 00:00:00 2001 From: Daniele Morotti <58258368+DanieleMorotti@users.noreply.github.com> Date: Fri, 23 May 2025 18:29:43 +0200 Subject: [PATCH 2/4] Changes requested by rm-openai --- src/agents/exceptions.py | 16 +++++++++------- src/agents/result.py | 22 +++++++++++++++++++++- src/agents/run.py | 31 +++++++++++++++++++------------ 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/agents/exceptions.py b/src/agents/exceptions.py index 93cd22aaa..533197f8d 100644 --- a/src/agents/exceptions.py +++ b/src/agents/exceptions.py @@ -1,4 +1,6 @@ -from typing import TYPE_CHECKING, Optional +from __future__ import annotations + +from typing import TYPE_CHECKING if TYPE_CHECKING: from .guardrail import InputGuardrailResult, OutputGuardrailResult @@ -13,9 +15,9 @@ class MaxTurnsExceeded(AgentsException): """Exception raised when the maximum number of turns is exceeded.""" message: str - run_error_details: Optional["RunErrorDetails"] + run_error_details: RunErrorDetails | None - def __init__(self, message: str, run_error_details: Optional["RunErrorDetails"] = None): + def __init__(self, message: str, run_error_details: RunErrorDetails | None = None): self.message = message self.run_error_details = run_error_details @@ -43,10 +45,10 @@ def __init__(self, message: str): class InputGuardrailTripwireTriggered(AgentsException): """Exception raised when a guardrail tripwire is triggered.""" - guardrail_result: "InputGuardrailResult" + guardrail_result: InputGuardrailResult """The result data of the guardrail that was triggered.""" - def __init__(self, guardrail_result: "InputGuardrailResult"): + def __init__(self, guardrail_result: InputGuardrailResult): self.guardrail_result = guardrail_result super().__init__( f"Guardrail {guardrail_result.guardrail.__class__.__name__} triggered tripwire" @@ -56,10 +58,10 @@ def __init__(self, guardrail_result: "InputGuardrailResult"): class OutputGuardrailTripwireTriggered(AgentsException): """Exception raised when a guardrail tripwire is triggered.""" - guardrail_result: "OutputGuardrailResult" + guardrail_result: OutputGuardrailResult """The result data of the guardrail that was triggered.""" - def __init__(self, guardrail_result: "OutputGuardrailResult"): + def __init__(self, guardrail_result: OutputGuardrailResult): self.guardrail_result = guardrail_result super().__init__( f"Guardrail {guardrail_result.guardrail.__class__.__name__} triggered tripwire" diff --git a/src/agents/result.py b/src/agents/result.py index cfab4dfce..e88eca0cb 100644 --- a/src/agents/result.py +++ b/src/agents/result.py @@ -249,8 +249,28 @@ def _cleanup_tasks(self): def __str__(self) -> str: return pretty_print_run_result_streaming(self) + @dataclass -class RunErrorDetails(RunResultBase): +class RunErrorDetails: + input: str | list[TResponseInputItem] + """The original input items i.e. the items before run() was called. This may be a mutated + version of the input, if there are handoff input filters that mutate the input. + """ + + new_items: list[RunItem] + """The new items generated during the agent run. These include things like new messages, tool + calls and their outputs, etc. + """ + + raw_responses: list[ModelResponse] + """The raw LLM responses generated by the model during the agent run.""" + + input_guardrail_results: list[InputGuardrailResult] + """Guardrail results for the input messages.""" + + context_wrapper: RunContextWrapper[Any] + """The context wrapper for the agent run.""" + _last_agent: Agent[Any] @property diff --git a/src/agents/run.py b/src/agents/run.py index 6b2ac7a74..023c85775 100644 --- a/src/agents/run.py +++ b/src/agents/run.py @@ -1,3 +1,4 @@ + from __future__ import annotations import asyncio @@ -208,19 +209,8 @@ async def run( data={"max_turns": max_turns}, ), ) - run_error_details = RunErrorDetails( - input=original_input, - new_items=generated_items, - raw_responses=model_responses, - final_output=None, - input_guardrail_results=input_guardrail_results, - output_guardrail_results=[], - context_wrapper=context_wrapper, - _last_agent=current_agent - ) raise MaxTurnsExceeded( - f"Max turns ({max_turns}) exceeded", - run_error_details + f"Max turns ({max_turns}) exceeded" ) logger.debug( @@ -296,6 +286,23 @@ async def run( raise AgentsException( f"Unknown next step type: {type(turn_result.next_step)}" ) + except Exception as e: + run_error_details = RunErrorDetails( + input=original_input, + new_items=generated_items, + raw_responses=model_responses, + input_guardrail_results=input_guardrail_results, + context_wrapper=context_wrapper, + _last_agent=current_agent + ) + # Re-raise with the error details + if isinstance(e, MaxTurnsExceeded): + raise MaxTurnsExceeded( + f"Max turns ({max_turns}) exceeded", + run_error_details + ) from e + else: + raise finally: if current_span: current_span.finish(reset_current=True) From f083c48e8a870e83036663a69acb41c9dba71ec7 Mon Sep 17 00:00:00 2001 From: Daniele Morotti <58258368+DanieleMorotti@users.noreply.github.com> Date: Sat, 24 May 2025 11:10:01 +0200 Subject: [PATCH 3/4] Return error details for streaming + tests --- src/agents/__init__.py | 3 +- src/agents/exceptions.py | 36 +++++++-- src/agents/result.py | 111 ++++++++++++++++---------- src/agents/run.py | 34 +++++--- src/agents/util/_pretty_print.py | 5 +- tests/test_run_error_details.py | 44 ++++++++++ tests/test_tracing_errors_streamed.py | 4 - 7 files changed, 169 insertions(+), 68 deletions(-) create mode 100644 tests/test_run_error_details.py diff --git a/src/agents/__init__.py b/src/agents/__init__.py index f36cefc69..820616437 100644 --- a/src/agents/__init__.py +++ b/src/agents/__init__.py @@ -14,6 +14,7 @@ MaxTurnsExceeded, ModelBehaviorError, OutputGuardrailTripwireTriggered, + RunErrorDetails, UserError, ) from .guardrail import ( @@ -44,7 +45,7 @@ from .models.openai_chatcompletions import OpenAIChatCompletionsModel from .models.openai_provider import OpenAIProvider from .models.openai_responses import OpenAIResponsesModel -from .result import RunErrorDetails, RunResult, RunResultStreaming +from .result import RunResult, RunResultStreaming from .run import RunConfig, Runner from .run_context import RunContextWrapper, TContext from .stream_events import ( diff --git a/src/agents/exceptions.py b/src/agents/exceptions.py index 533197f8d..4f6e2e768 100644 --- a/src/agents/exceptions.py +++ b/src/agents/exceptions.py @@ -1,25 +1,49 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: + from .agent import Agent from .guardrail import InputGuardrailResult, OutputGuardrailResult - from .result import RunErrorDetails + from .items import ModelResponse, RunItem, TResponseInputItem + from .run_context import RunContextWrapper + +from .util._pretty_print import pretty_print_run_error_details + + +@dataclass +class RunErrorDetails: + """Data collected from an agent run when an exception occurs.""" + input: str | list[TResponseInputItem] + new_items: list[RunItem] + raw_responses: list[ModelResponse] + last_agent: Agent[Any] + context_wrapper: RunContextWrapper[Any] + input_guardrail_results: list[InputGuardrailResult] + output_guardrail_results: list[OutputGuardrailResult] + + def __str__(self) -> str: + return pretty_print_run_error_details(self) class AgentsException(Exception): """Base class for all exceptions in the Agents SDK.""" + run_data: RunErrorDetails | None + + def __init__(self, *args: object) -> None: + super().__init__(*args) + self.run_data = None class MaxTurnsExceeded(AgentsException): """Exception raised when the maximum number of turns is exceeded.""" message: str - run_error_details: RunErrorDetails | None - def __init__(self, message: str, run_error_details: RunErrorDetails | None = None): + def __init__(self, message: str): self.message = message - self.run_error_details = run_error_details + super().__init__(message) class ModelBehaviorError(AgentsException): @@ -31,6 +55,7 @@ class ModelBehaviorError(AgentsException): def __init__(self, message: str): self.message = message + super().__init__(message) class UserError(AgentsException): @@ -40,6 +65,7 @@ class UserError(AgentsException): def __init__(self, message: str): self.message = message + super().__init__(message) class InputGuardrailTripwireTriggered(AgentsException): diff --git a/src/agents/result.py b/src/agents/result.py index e88eca0cb..e699f1caa 100644 --- a/src/agents/result.py +++ b/src/agents/result.py @@ -11,7 +11,12 @@ from ._run_impl import QueueCompleteSentinel from .agent import Agent from .agent_output import AgentOutputSchemaBase -from .exceptions import InputGuardrailTripwireTriggered, MaxTurnsExceeded +from .exceptions import ( + AgentsException, + InputGuardrailTripwireTriggered, + MaxTurnsExceeded, + RunErrorDetails, +) from .guardrail import InputGuardrailResult, OutputGuardrailResult from .items import ItemHelpers, ModelResponse, RunItem, TResponseInputItem from .logger import logger @@ -20,7 +25,6 @@ from .tracing import Trace from .util._pretty_print import ( pretty_print_result, - pretty_print_run_error_details, pretty_print_run_result_streaming, ) @@ -212,29 +216,79 @@ async def stream_events(self) -> AsyncIterator[StreamEvent]: def _check_errors(self): if self.current_turn > self.max_turns: - self._stored_exception = MaxTurnsExceeded(f"Max turns ({self.max_turns}) exceeded") + max_turns_exc = MaxTurnsExceeded(f"Max turns ({self.max_turns}) exceeded") + max_turns_exc.run_data = RunErrorDetails( + input=self.input, + new_items=self.new_items, + raw_responses=self.raw_responses, + last_agent=self.current_agent, + context_wrapper=self.context_wrapper, + input_guardrail_results=self.input_guardrail_results, + output_guardrail_results=self.output_guardrail_results, + ) + self._stored_exception = max_turns_exc # Fetch all the completed guardrail results from the queue and raise if needed while not self._input_guardrail_queue.empty(): guardrail_result = self._input_guardrail_queue.get_nowait() if guardrail_result.output.tripwire_triggered: - self._stored_exception = InputGuardrailTripwireTriggered(guardrail_result) + tripwire_exc = InputGuardrailTripwireTriggered(guardrail_result) + tripwire_exc.run_data = RunErrorDetails( + input=self.input, + new_items=self.new_items, + raw_responses=self.raw_responses, + last_agent=self.current_agent, + context_wrapper=self.context_wrapper, + input_guardrail_results=self.input_guardrail_results, + output_guardrail_results=self.output_guardrail_results, + ) + self._stored_exception = tripwire_exc # Check the tasks for any exceptions if self._run_impl_task and self._run_impl_task.done(): - exc = self._run_impl_task.exception() - if exc and isinstance(exc, Exception): - self._stored_exception = exc + run_impl_exc = self._run_impl_task.exception() + if run_impl_exc and isinstance(run_impl_exc, Exception): + if isinstance(run_impl_exc, AgentsException) and run_impl_exc.run_data is None: + run_impl_exc.run_data = RunErrorDetails( + input=self.input, + new_items=self.new_items, + raw_responses=self.raw_responses, + last_agent=self.current_agent, + context_wrapper=self.context_wrapper, + input_guardrail_results=self.input_guardrail_results, + output_guardrail_results=self.output_guardrail_results, + ) + self._stored_exception = run_impl_exc if self._input_guardrails_task and self._input_guardrails_task.done(): - exc = self._input_guardrails_task.exception() - if exc and isinstance(exc, Exception): - self._stored_exception = exc + in_guard_exc = self._input_guardrails_task.exception() + if in_guard_exc and isinstance(in_guard_exc, Exception): + if isinstance(in_guard_exc, AgentsException) and in_guard_exc.run_data is None: + in_guard_exc.run_data = RunErrorDetails( + input=self.input, + new_items=self.new_items, + raw_responses=self.raw_responses, + last_agent=self.current_agent, + context_wrapper=self.context_wrapper, + input_guardrail_results=self.input_guardrail_results, + output_guardrail_results=self.output_guardrail_results, + ) + self._stored_exception = in_guard_exc if self._output_guardrails_task and self._output_guardrails_task.done(): - exc = self._output_guardrails_task.exception() - if exc and isinstance(exc, Exception): - self._stored_exception = exc + out_guard_exc = self._output_guardrails_task.exception() + if out_guard_exc and isinstance(out_guard_exc, Exception): + if isinstance(out_guard_exc, AgentsException) and out_guard_exc.run_data is None: + out_guard_exc.run_data = RunErrorDetails( + input=self.input, + new_items=self.new_items, + raw_responses=self.raw_responses, + last_agent=self.current_agent, + context_wrapper=self.context_wrapper, + input_guardrail_results=self.input_guardrail_results, + output_guardrail_results=self.output_guardrail_results, + ) + self._stored_exception = out_guard_exc def _cleanup_tasks(self): if self._run_impl_task and not self._run_impl_task.done(): @@ -249,34 +303,3 @@ def _cleanup_tasks(self): def __str__(self) -> str: return pretty_print_run_result_streaming(self) - -@dataclass -class RunErrorDetails: - input: str | list[TResponseInputItem] - """The original input items i.e. the items before run() was called. This may be a mutated - version of the input, if there are handoff input filters that mutate the input. - """ - - new_items: list[RunItem] - """The new items generated during the agent run. These include things like new messages, tool - calls and their outputs, etc. - """ - - raw_responses: list[ModelResponse] - """The raw LLM responses generated by the model during the agent run.""" - - input_guardrail_results: list[InputGuardrailResult] - """Guardrail results for the input messages.""" - - context_wrapper: RunContextWrapper[Any] - """The context wrapper for the agent run.""" - - _last_agent: Agent[Any] - - @property - def last_agent(self) -> Agent[Any]: - """The last agent that was run.""" - return self._last_agent - - def __str__(self) -> str: - return pretty_print_run_error_details(self) diff --git a/src/agents/run.py b/src/agents/run.py index 023c85775..c67386495 100644 --- a/src/agents/run.py +++ b/src/agents/run.py @@ -27,6 +27,7 @@ MaxTurnsExceeded, ModelBehaviorError, OutputGuardrailTripwireTriggered, + RunErrorDetails, ) from .guardrail import InputGuardrail, InputGuardrailResult, OutputGuardrail, OutputGuardrailResult from .handoffs import Handoff, HandoffInputFilter, handoff @@ -36,7 +37,7 @@ from .model_settings import ModelSettings from .models.interface import Model, ModelProvider from .models.multi_provider import MultiProvider -from .result import RunErrorDetails, RunResult, RunResultStreaming +from .result import RunResult, RunResultStreaming from .run_context import RunContextWrapper, TContext from .stream_events import AgentUpdatedStreamEvent, RawResponsesStreamEvent from .tool import Tool @@ -286,23 +287,17 @@ async def run( raise AgentsException( f"Unknown next step type: {type(turn_result.next_step)}" ) - except Exception as e: - run_error_details = RunErrorDetails( + except AgentsException as exc: + exc.run_data = RunErrorDetails( input=original_input, new_items=generated_items, raw_responses=model_responses, - input_guardrail_results=input_guardrail_results, + last_agent=current_agent, context_wrapper=context_wrapper, - _last_agent=current_agent + input_guardrail_results=input_guardrail_results, + output_guardrail_results=[] ) - # Re-raise with the error details - if isinstance(e, MaxTurnsExceeded): - raise MaxTurnsExceeded( - f"Max turns ({max_turns}) exceeded", - run_error_details - ) from e - else: - raise + raise finally: if current_span: current_span.finish(reset_current=True) @@ -629,6 +624,19 @@ async def _run_streamed_impl( streamed_result._event_queue.put_nowait(QueueCompleteSentinel()) elif isinstance(turn_result.next_step, NextStepRunAgain): pass + except AgentsException as exc: + streamed_result.is_complete = True + streamed_result._event_queue.put_nowait(QueueCompleteSentinel()) + exc.run_data = RunErrorDetails( + input=streamed_result.input, + new_items=streamed_result.new_items, + raw_responses=streamed_result.raw_responses, + last_agent=current_agent, + context_wrapper=context_wrapper, + input_guardrail_results=streamed_result.input_guardrail_results, + output_guardrail_results=streamed_result.output_guardrail_results, + ) + raise except Exception as e: if current_span: _error_tracing.attach_error_to_span( diff --git a/src/agents/util/_pretty_print.py b/src/agents/util/_pretty_print.py index 69a79740e..29df3562e 100644 --- a/src/agents/util/_pretty_print.py +++ b/src/agents/util/_pretty_print.py @@ -3,7 +3,8 @@ from pydantic import BaseModel if TYPE_CHECKING: - from ..result import RunErrorDetails, RunResult, RunResultBase, RunResultStreaming + from ..exceptions import RunErrorDetails + from ..result import RunResult, RunResultBase, RunResultStreaming def _indent(text: str, indent_level: int) -> str: @@ -37,6 +38,7 @@ def pretty_print_result(result: "RunResult") -> str: return output + def pretty_print_run_error_details(result: "RunErrorDetails") -> str: output = "RunErrorDetails:" output += f'\n- Last agent: Agent(name="{result.last_agent.name}", ...)' @@ -47,6 +49,7 @@ def pretty_print_run_error_details(result: "RunErrorDetails") -> str: return output + def pretty_print_run_result_streaming(result: "RunResultStreaming") -> str: output = "RunResultStreaming:" output += f'\n- Current agent: Agent(name="{result.current_agent.name}", ...)' diff --git a/tests/test_run_error_details.py b/tests/test_run_error_details.py new file mode 100644 index 000000000..2268b3780 --- /dev/null +++ b/tests/test_run_error_details.py @@ -0,0 +1,44 @@ +import json + +import pytest + +from agents import Agent, MaxTurnsExceeded, RunErrorDetails, Runner + +from .fake_model import FakeModel +from .test_responses import get_function_tool, get_function_tool_call, get_text_message + + +@pytest.mark.asyncio +async def test_run_error_includes_data(): + model = FakeModel() + agent = Agent(name="test", model=model, tools=[get_function_tool("foo", "res")]) + model.add_multiple_turn_outputs([ + [get_text_message("1"), get_function_tool_call("foo", json.dumps({"a": "b"}))], + [get_text_message("done")], + ]) + with pytest.raises(MaxTurnsExceeded) as exc: + await Runner.run(agent, input="hello", max_turns=1) + data = exc.value.run_data + assert isinstance(data, RunErrorDetails) + assert data.last_agent == agent + assert len(data.raw_responses) == 1 + assert len(data.new_items) > 0 + + +@pytest.mark.asyncio +async def test_streamed_run_error_includes_data(): + model = FakeModel() + agent = Agent(name="test", model=model, tools=[get_function_tool("foo", "res")]) + model.add_multiple_turn_outputs([ + [get_text_message("1"), get_function_tool_call("foo", json.dumps({"a": "b"}))], + [get_text_message("done")], + ]) + result = Runner.run_streamed(agent, input="hello", max_turns=1) + with pytest.raises(MaxTurnsExceeded) as exc: + async for _ in result.stream_events(): + pass + data = exc.value.run_data + assert isinstance(data, RunErrorDetails) + assert data.last_agent == agent + assert len(data.raw_responses) == 1 + assert len(data.new_items) > 0 diff --git a/tests/test_tracing_errors_streamed.py b/tests/test_tracing_errors_streamed.py index 416793e70..40efef3fa 100644 --- a/tests/test_tracing_errors_streamed.py +++ b/tests/test_tracing_errors_streamed.py @@ -168,10 +168,6 @@ async def test_tool_call_error(): "children": [ { "type": "agent", - "error": { - "message": "Error in agent run", - "data": {"error": "Invalid JSON input for tool foo: bad_json"}, - }, "data": { "name": "test_agent", "handoffs": [], From 673668730aaba411478b9705545e528ef1d24561 Mon Sep 17 00:00:00 2001 From: Daniele Morotti <58258368+DanieleMorotti@users.noreply.github.com> Date: Wed, 28 May 2025 12:50:21 +0200 Subject: [PATCH 4/4] Implemented utility function to create `RunErrorDetails` avoiding code duplication --- src/agents/result.py | 62 ++++++++++++-------------------------------- 1 file changed, 17 insertions(+), 45 deletions(-) diff --git a/src/agents/result.py b/src/agents/result.py index e699f1caa..764815246 100644 --- a/src/agents/result.py +++ b/src/agents/result.py @@ -214,18 +214,22 @@ async def stream_events(self) -> AsyncIterator[StreamEvent]: if self._stored_exception: raise self._stored_exception + def _create_error_details(self) -> RunErrorDetails: + """Return a `RunErrorDetails` object considering the current attributes of the class.""" + return RunErrorDetails( + input=self.input, + new_items=self.new_items, + raw_responses=self.raw_responses, + last_agent=self.current_agent, + context_wrapper=self.context_wrapper, + input_guardrail_results=self.input_guardrail_results, + output_guardrail_results=self.output_guardrail_results, + ) + def _check_errors(self): if self.current_turn > self.max_turns: max_turns_exc = MaxTurnsExceeded(f"Max turns ({self.max_turns}) exceeded") - max_turns_exc.run_data = RunErrorDetails( - input=self.input, - new_items=self.new_items, - raw_responses=self.raw_responses, - last_agent=self.current_agent, - context_wrapper=self.context_wrapper, - input_guardrail_results=self.input_guardrail_results, - output_guardrail_results=self.output_guardrail_results, - ) + max_turns_exc.run_data = self._create_error_details() self._stored_exception = max_turns_exc # Fetch all the completed guardrail results from the queue and raise if needed @@ -233,15 +237,7 @@ def _check_errors(self): guardrail_result = self._input_guardrail_queue.get_nowait() if guardrail_result.output.tripwire_triggered: tripwire_exc = InputGuardrailTripwireTriggered(guardrail_result) - tripwire_exc.run_data = RunErrorDetails( - input=self.input, - new_items=self.new_items, - raw_responses=self.raw_responses, - last_agent=self.current_agent, - context_wrapper=self.context_wrapper, - input_guardrail_results=self.input_guardrail_results, - output_guardrail_results=self.output_guardrail_results, - ) + tripwire_exc.run_data = self._create_error_details() self._stored_exception = tripwire_exc # Check the tasks for any exceptions @@ -249,45 +245,21 @@ def _check_errors(self): run_impl_exc = self._run_impl_task.exception() if run_impl_exc and isinstance(run_impl_exc, Exception): if isinstance(run_impl_exc, AgentsException) and run_impl_exc.run_data is None: - run_impl_exc.run_data = RunErrorDetails( - input=self.input, - new_items=self.new_items, - raw_responses=self.raw_responses, - last_agent=self.current_agent, - context_wrapper=self.context_wrapper, - input_guardrail_results=self.input_guardrail_results, - output_guardrail_results=self.output_guardrail_results, - ) + run_impl_exc.run_data = self._create_error_details() self._stored_exception = run_impl_exc if self._input_guardrails_task and self._input_guardrails_task.done(): in_guard_exc = self._input_guardrails_task.exception() if in_guard_exc and isinstance(in_guard_exc, Exception): if isinstance(in_guard_exc, AgentsException) and in_guard_exc.run_data is None: - in_guard_exc.run_data = RunErrorDetails( - input=self.input, - new_items=self.new_items, - raw_responses=self.raw_responses, - last_agent=self.current_agent, - context_wrapper=self.context_wrapper, - input_guardrail_results=self.input_guardrail_results, - output_guardrail_results=self.output_guardrail_results, - ) + in_guard_exc.run_data = self._create_error_details() self._stored_exception = in_guard_exc if self._output_guardrails_task and self._output_guardrails_task.done(): out_guard_exc = self._output_guardrails_task.exception() if out_guard_exc and isinstance(out_guard_exc, Exception): if isinstance(out_guard_exc, AgentsException) and out_guard_exc.run_data is None: - out_guard_exc.run_data = RunErrorDetails( - input=self.input, - new_items=self.new_items, - raw_responses=self.raw_responses, - last_agent=self.current_agent, - context_wrapper=self.context_wrapper, - input_guardrail_results=self.input_guardrail_results, - output_guardrail_results=self.output_guardrail_results, - ) + out_guard_exc.run_data = self._create_error_details() self._stored_exception = out_guard_exc def _cleanup_tasks(self):