Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/agents/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,7 @@ def _finalize_result(result: RunResult) -> RunResult:
)
handler_result = await resolve_run_error_handler_result(
error_handlers=error_handlers,
error_kind="max_turns",
error=max_turns_error,
context_wrapper=context_wrapper,
run_data=run_error_data,
Expand Down
5 changes: 3 additions & 2 deletions src/agents/run_error_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing_extensions import TypedDict

from .agent import Agent
from .exceptions import MaxTurnsExceeded, ModelRefusalError
from .exceptions import MaxTurnsExceeded, ModelBehaviorError, ModelRefusalError
from .items import ModelResponse, RunItem, TResponseInputItem
from .run_context import RunContextWrapper, TContext
from .util._types import MaybeAwaitable
Expand All @@ -27,7 +27,7 @@ class RunErrorData:

@dataclass
class RunErrorHandlerInput(Generic[TContext]):
error: MaxTurnsExceeded | ModelRefusalError
error: MaxTurnsExceeded | ModelRefusalError | ModelBehaviorError
context: RunContextWrapper[TContext]
run_data: RunErrorData

Expand All @@ -52,6 +52,7 @@ class RunErrorHandlers(TypedDict, Generic[TContext], total=False):

max_turns: RunErrorHandler[TContext]
model_refusal: RunErrorHandler[TContext]
invalid_final_output: RunErrorHandler[TContext]
Comment thread
seratch marked this conversation as resolved.


__all__ = [
Expand Down
12 changes: 6 additions & 6 deletions src/agents/run_internal/error_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import inspect
import json
from typing import Any
from typing import Any, Literal

from openai.types.responses import ResponseOutputMessage, ResponseOutputText

Expand All @@ -27,6 +27,8 @@
from .items import ReasoningItemIdPolicy, run_item_to_input_item
from .turn_preparation import get_output_schema

RunErrorHandlerKind = Literal["max_turns", "model_refusal", "invalid_final_output"]


def build_run_error_data(
*,
Expand Down Expand Up @@ -128,16 +130,14 @@ def create_message_output_item(agent: Agent[Any], output_text: str) -> MessageOu
async def resolve_run_error_handler_result(
*,
error_handlers: RunErrorHandlers[TContext] | None,
error: MaxTurnsExceeded | ModelRefusalError,
error_kind: RunErrorHandlerKind,
error: MaxTurnsExceeded | ModelRefusalError | ModelBehaviorError,
context_wrapper: RunContextWrapper[TContext],
run_data: RunErrorData,
) -> RunErrorHandlerResult | None:
if not error_handlers:
return None
if isinstance(error, ModelRefusalError):
handler = error_handlers.get("model_refusal")
else:
handler = error_handlers.get("max_turns")
handler = error_handlers.get(error_kind)
if handler is None:
return None
handler_input = RunErrorHandlerInput(
Expand Down
1 change: 1 addition & 0 deletions src/agents/run_internal/run_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,7 @@ async def _save_stream_items_without_count(
)
handler_result = await resolve_run_error_handler_result(
error_handlers=error_handlers,
error_kind="max_turns",
error=max_turns_error,
context_wrapper=context_wrapper,
run_data=run_error_data,
Expand Down
86 changes: 84 additions & 2 deletions src/agents/run_internal/turn_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,44 @@ async def execute_final_output(
)


async def _resolve_invalid_final_output(
*,
error_handlers: RunErrorHandlers[TContext] | None,
error: ModelBehaviorError,
public_agent: Agent[TContext],
original_input: str | list[TResponseInputItem],
new_response: ModelResponse,
new_items: list[RunItem],
context_wrapper: RunContextWrapper[TContext],
) -> tuple[Any, MessageOutputItem | None] | None:
run_error_data = build_run_error_data(
input=original_input,
new_items=new_items,
raw_responses=[new_response],
last_agent=public_agent,
)
handler_result = await resolve_run_error_handler_result(
error_handlers=error_handlers,
error_kind="invalid_final_output",
error=error,
context_wrapper=context_wrapper,
run_data=run_error_data,
)
if handler_result is None:
return None

final_output = validate_handler_final_output(public_agent, handler_result.final_output)
message_item = (
create_message_output_item(
public_agent,
format_final_output_text(public_agent, final_output),
)
if handler_result.include_in_history
else None
)
return final_output, message_item


def _resolve_server_managed_handoff_behavior(
*,
handoff: Handoff[Any, Agent[Any]],
Expand Down Expand Up @@ -781,6 +819,7 @@ async def execute_tools_and_side_effects(
)
handler_result = await resolve_run_error_handler_result(
error_handlers=error_handlers,
error_kind="model_refusal",
error=refusal_error,
context_wrapper=context_wrapper,
run_data=run_error_data,
Expand All @@ -806,8 +845,51 @@ async def execute_tools_and_side_effects(
tool_input_guardrail_results=tool_input_guardrail_results,
tool_output_guardrail_results=tool_output_guardrail_results,
)
if output_schema and not output_schema.is_plain_text() and potential_final_output_text:
final_output = output_schema.validate_json(potential_final_output_text)
if output_schema and not output_schema.is_plain_text():
if potential_final_output_text:
try:
final_output = output_schema.validate_json(potential_final_output_text)
except ModelBehaviorError as error:
resolved_handler_output = await _resolve_invalid_final_output(
error_handlers=error_handlers,
error=error,
public_agent=public_agent,
original_input=original_input,
new_response=new_response,
new_items=pre_step_items + new_step_items,
context_wrapper=context_wrapper,
)
if resolved_handler_output is None:
raise
final_output, message_item = resolved_handler_output
if message_item is not None:
new_step_items.append(message_item)
else:
resolved_handler_output = await _resolve_invalid_final_output(
error_handlers=error_handlers,
error=ModelBehaviorError(
"Model returned no final output for the structured output type."
),
public_agent=public_agent,
original_input=original_input,
new_response=new_response,
new_items=pre_step_items + new_step_items,
context_wrapper=context_wrapper,
)
if resolved_handler_output is None:
return SingleStepResult(
original_input=original_input,
model_response=new_response,
pre_step_items=pre_step_items,
new_step_items=new_step_items,
next_step=NextStepRunAgain(),
tool_input_guardrail_results=tool_input_guardrail_results,
tool_output_guardrail_results=tool_output_guardrail_results,
)
final_output, message_item = resolved_handler_output
if message_item is not None:
new_step_items.append(message_item)

return await execute_final_output_call(
public_agent=public_agent,
original_input=original_input,
Expand Down
Loading