11from __future__ import annotations
22
33import asyncio
4+ import inspect
5+ from collections .abc import Awaitable
46from dataclasses import dataclass
5- from typing import TYPE_CHECKING , Any
7+ from typing import TYPE_CHECKING , Any , cast
68
79from openai .types .responses import (
810 ResponseComputerToolCall ,
2527from openai .types .responses .response_input_param import ComputerCallOutput
2628from openai .types .responses .response_reasoning_item import ResponseReasoningItem
2729
28- from .agent import Agent
30+ from .agent import Agent , ToolsToFinalOutputResult
2931from .agent_output import AgentOutputSchema
3032from .computer import AsyncComputer , Computer
3133from .exceptions import AgentsException , ModelBehaviorError , UserError
4850from .models .interface import ModelTracing
4951from .run_context import RunContextWrapper , TContext
5052from .stream_events import RunItemStreamEvent , StreamEvent
51- from .tool import ComputerTool , FunctionTool
53+ from .tool import ComputerTool , FunctionTool , FunctionToolResult
5254from .tracing import (
5355 SpanError ,
5456 Trace ,
@@ -70,6 +72,8 @@ class QueueCompleteSentinel:
7072
7173QUEUE_COMPLETE_SENTINEL = QueueCompleteSentinel ()
7274
75+ _NOT_FINAL_OUTPUT = ToolsToFinalOutputResult (is_final_output = False , final_output = None )
76+
7377
7478@dataclass
7579class ToolRunHandoff :
@@ -199,7 +203,7 @@ async def execute_tools_and_side_effects(
199203 config = run_config ,
200204 ),
201205 )
202- new_step_items .extend (function_results )
206+ new_step_items .extend ([ result . run_item for result in function_results ] )
203207 new_step_items .extend (computer_results )
204208
205209 # Second, check if there are any handoffs
@@ -216,6 +220,36 @@ async def execute_tools_and_side_effects(
216220 run_config = run_config ,
217221 )
218222
223+ # Third, we'll check if the tool use should result in a final output
224+ check_tool_use = await cls ._check_for_final_output_from_tools (
225+ agent = agent ,
226+ tool_results = function_results ,
227+ context_wrapper = context_wrapper ,
228+ config = run_config ,
229+ )
230+
231+ if check_tool_use .is_final_output :
232+ # If the output type is str, then let's just stringify it
233+ if not agent .output_type or agent .output_type is str :
234+ check_tool_use .final_output = str (check_tool_use .final_output )
235+
236+ if check_tool_use .final_output is None :
237+ logger .error (
238+ "Model returned a final output of None. Not raising an error because we assume"
239+ "you know what you're doing."
240+ )
241+
242+ return await cls .execute_final_output (
243+ agent = agent ,
244+ original_input = original_input ,
245+ new_response = new_response ,
246+ pre_step_items = pre_step_items ,
247+ new_step_items = new_step_items ,
248+ final_output = check_tool_use .final_output ,
249+ hooks = hooks ,
250+ context_wrapper = context_wrapper ,
251+ )
252+
219253 # Now we can check if the model also produced a final output
220254 message_items = [item for item in new_step_items if isinstance (item , MessageOutputItem )]
221255
@@ -355,10 +389,10 @@ async def execute_function_tool_calls(
355389 hooks : RunHooks [TContext ],
356390 context_wrapper : RunContextWrapper [TContext ],
357391 config : RunConfig ,
358- ) -> list [RunItem ]:
392+ ) -> list [FunctionToolResult ]:
359393 async def run_single_tool (
360394 func_tool : FunctionTool , tool_call : ResponseFunctionToolCall
361- ) -> str :
395+ ) -> Any :
362396 with function_span (func_tool .name ) as span_fn :
363397 if config .trace_include_sensitive_data :
364398 span_fn .span_data .input = tool_call .arguments
@@ -404,10 +438,14 @@ async def run_single_tool(
404438 results = await asyncio .gather (* tasks )
405439
406440 return [
407- ToolCallOutputItem (
408- output = str (result ),
409- raw_item = ItemHelpers .tool_call_output_item (tool_run .tool_call , str (result )),
410- agent = agent ,
441+ FunctionToolResult (
442+ tool = tool_run .function_tool ,
443+ output = result ,
444+ run_item = ToolCallOutputItem (
445+ output = result ,
446+ raw_item = ItemHelpers .tool_call_output_item (tool_run .tool_call , str (result )),
447+ agent = agent ,
448+ ),
411449 )
412450 for tool_run , result in zip (tool_runs , results )
413451 ]
@@ -646,6 +684,47 @@ def stream_step_result_to_queue(
646684 if event :
647685 queue .put_nowait (event )
648686
687+ @classmethod
688+ async def _check_for_final_output_from_tools (
689+ cls ,
690+ * ,
691+ agent : Agent [TContext ],
692+ tool_results : list [FunctionToolResult ],
693+ context_wrapper : RunContextWrapper [TContext ],
694+ config : RunConfig ,
695+ ) -> ToolsToFinalOutputResult :
696+ """Returns (i, final_output)."""
697+ if not tool_results :
698+ return _NOT_FINAL_OUTPUT
699+
700+ if agent .tool_use_behavior == "run_llm_again" :
701+ return _NOT_FINAL_OUTPUT
702+ elif agent .tool_use_behavior == "stop_on_first_tool" :
703+ return ToolsToFinalOutputResult (
704+ is_final_output = True , final_output = tool_results [0 ].output
705+ )
706+ elif isinstance (agent .tool_use_behavior , dict ):
707+ names = agent .tool_use_behavior .get ("stop_at_tool_names" , [])
708+ for tool_result in tool_results :
709+ if tool_result .tool .name in names :
710+ return ToolsToFinalOutputResult (
711+ is_final_output = True , final_output = tool_result .output
712+ )
713+ return ToolsToFinalOutputResult (is_final_output = False , final_output = None )
714+ elif callable (agent .tool_use_behavior ):
715+ if inspect .iscoroutinefunction (agent .tool_use_behavior ):
716+ return await cast (
717+ Awaitable [ToolsToFinalOutputResult ],
718+ agent .tool_use_behavior (context_wrapper , tool_results ),
719+ )
720+ else :
721+ return cast (
722+ ToolsToFinalOutputResult , agent .tool_use_behavior (context_wrapper , tool_results )
723+ )
724+
725+ logger .error (f"Invalid tool_use_behavior: { agent .tool_use_behavior } " )
726+ raise UserError (f"Invalid tool_use_behavior: { agent .tool_use_behavior } " )
727+
649728
650729class TraceCtxManager :
651730 """Creates a trace only if there is no current trace, and manages the trace lifecycle."""
0 commit comments