1
1
from __future__ import annotations
2
2
3
3
import asyncio
4
+ import inspect
5
+ from collections .abc import Awaitable
4
6
from dataclasses import dataclass
5
- from typing import TYPE_CHECKING , Any
7
+ from typing import TYPE_CHECKING , Any , cast
6
8
7
9
from openai .types .responses import (
8
10
ResponseComputerToolCall ,
25
27
from openai .types .responses .response_input_param import ComputerCallOutput
26
28
from openai .types .responses .response_reasoning_item import ResponseReasoningItem
27
29
28
- from .agent import Agent
30
+ from .agent import Agent , ToolsToFinalOutputResult
29
31
from .agent_output import AgentOutputSchema
30
32
from .computer import AsyncComputer , Computer
31
33
from .exceptions import AgentsException , ModelBehaviorError , UserError
48
50
from .models .interface import ModelTracing
49
51
from .run_context import RunContextWrapper , TContext
50
52
from .stream_events import RunItemStreamEvent , StreamEvent
51
- from .tool import ComputerTool , FunctionTool
53
+ from .tool import ComputerTool , FunctionTool , FunctionToolResult
52
54
from .tracing import (
53
55
SpanError ,
54
56
Trace ,
@@ -70,6 +72,8 @@ class QueueCompleteSentinel:
70
72
71
73
QUEUE_COMPLETE_SENTINEL = QueueCompleteSentinel ()
72
74
75
+ _NOT_FINAL_OUTPUT = ToolsToFinalOutputResult (is_final_output = False , final_output = None )
76
+
73
77
74
78
@dataclass
75
79
class ToolRunHandoff :
@@ -199,7 +203,7 @@ async def execute_tools_and_side_effects(
199
203
config = run_config ,
200
204
),
201
205
)
202
- new_step_items .extend (function_results )
206
+ new_step_items .extend ([ result . run_item for result in function_results ] )
203
207
new_step_items .extend (computer_results )
204
208
205
209
# Second, check if there are any handoffs
@@ -216,6 +220,36 @@ async def execute_tools_and_side_effects(
216
220
run_config = run_config ,
217
221
)
218
222
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
+
219
253
# Now we can check if the model also produced a final output
220
254
message_items = [item for item in new_step_items if isinstance (item , MessageOutputItem )]
221
255
@@ -355,10 +389,10 @@ async def execute_function_tool_calls(
355
389
hooks : RunHooks [TContext ],
356
390
context_wrapper : RunContextWrapper [TContext ],
357
391
config : RunConfig ,
358
- ) -> list [RunItem ]:
392
+ ) -> list [FunctionToolResult ]:
359
393
async def run_single_tool (
360
394
func_tool : FunctionTool , tool_call : ResponseFunctionToolCall
361
- ) -> str :
395
+ ) -> Any :
362
396
with function_span (func_tool .name ) as span_fn :
363
397
if config .trace_include_sensitive_data :
364
398
span_fn .span_data .input = tool_call .arguments
@@ -404,10 +438,14 @@ async def run_single_tool(
404
438
results = await asyncio .gather (* tasks )
405
439
406
440
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
+ ),
411
449
)
412
450
for tool_run , result in zip (tool_runs , results )
413
451
]
@@ -646,6 +684,47 @@ def stream_step_result_to_queue(
646
684
if event :
647
685
queue .put_nowait (event )
648
686
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
+
649
728
650
729
class TraceCtxManager :
651
730
"""Creates a trace only if there is no current trace, and manages the trace lifecycle."""
0 commit comments