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,30 @@ 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
+ return await cls .execute_final_output (
237
+ agent = agent ,
238
+ original_input = original_input ,
239
+ new_response = new_response ,
240
+ pre_step_items = pre_step_items ,
241
+ new_step_items = new_step_items ,
242
+ final_output = check_tool_use .final_output ,
243
+ hooks = hooks ,
244
+ context_wrapper = context_wrapper ,
245
+ )
246
+
219
247
# Now we can check if the model also produced a final output
220
248
message_items = [item for item in new_step_items if isinstance (item , MessageOutputItem )]
221
249
@@ -355,10 +383,10 @@ async def execute_function_tool_calls(
355
383
hooks : RunHooks [TContext ],
356
384
context_wrapper : RunContextWrapper [TContext ],
357
385
config : RunConfig ,
358
- ) -> list [RunItem ]:
386
+ ) -> list [FunctionToolResult ]:
359
387
async def run_single_tool (
360
388
func_tool : FunctionTool , tool_call : ResponseFunctionToolCall
361
- ) -> str :
389
+ ) -> Any :
362
390
with function_span (func_tool .name ) as span_fn :
363
391
if config .trace_include_sensitive_data :
364
392
span_fn .span_data .input = tool_call .arguments
@@ -404,10 +432,14 @@ async def run_single_tool(
404
432
results = await asyncio .gather (* tasks )
405
433
406
434
return [
407
- ToolCallOutputItem (
408
- output = str (result ),
409
- raw_item = ItemHelpers .tool_call_output_item (tool_run .tool_call , str (result )),
410
- agent = agent ,
435
+ FunctionToolResult (
436
+ tool = tool_run .function_tool ,
437
+ output = result ,
438
+ run_item = ToolCallOutputItem (
439
+ output = result ,
440
+ raw_item = ItemHelpers .tool_call_output_item (tool_run .tool_call , str (result )),
441
+ agent = agent ,
442
+ ),
411
443
)
412
444
for tool_run , result in zip (tool_runs , results )
413
445
]
@@ -646,6 +678,39 @@ def stream_step_result_to_queue(
646
678
if event :
647
679
queue .put_nowait (event )
648
680
681
+ @classmethod
682
+ async def _check_for_final_output_from_tools (
683
+ cls ,
684
+ * ,
685
+ agent : Agent [TContext ],
686
+ tool_results : list [FunctionToolResult ],
687
+ context_wrapper : RunContextWrapper [TContext ],
688
+ config : RunConfig ,
689
+ ) -> ToolsToFinalOutputResult :
690
+ """Returns (i, final_output)."""
691
+ if not tool_results :
692
+ return _NOT_FINAL_OUTPUT
693
+
694
+ if agent .tool_use_behavior == "run_llm_again" :
695
+ return _NOT_FINAL_OUTPUT
696
+ elif agent .tool_use_behavior == "stop_on_first_tool" :
697
+ return ToolsToFinalOutputResult (
698
+ is_final_output = True , final_output = tool_results [0 ].output
699
+ )
700
+ elif callable (agent .tool_use_behavior ):
701
+ if inspect .iscoroutinefunction (agent .tool_use_behavior ):
702
+ return await cast (
703
+ Awaitable [ToolsToFinalOutputResult ],
704
+ agent .tool_use_behavior (context_wrapper , tool_results ),
705
+ )
706
+ else :
707
+ return cast (
708
+ ToolsToFinalOutputResult , agent .tool_use_behavior (context_wrapper , tool_results )
709
+ )
710
+ else :
711
+ logger .error (f"Invalid tool_use_behavior: { agent .tool_use_behavior } " )
712
+ raise UserError (f"Invalid tool_use_behavior: { agent .tool_use_behavior } " )
713
+
649
714
650
715
class TraceCtxManager :
651
716
"""Creates a trace only if there is no current trace, and manages the trace lifecycle."""
0 commit comments