14
14
ResponseFunctionWebSearch ,
15
15
ResponseOutputMessage ,
16
16
)
17
+ from openai .types .responses .response_code_interpreter_tool_call import (
18
+ ResponseCodeInterpreterToolCall ,
19
+ )
17
20
from openai .types .responses .response_computer_tool_call import (
18
21
ActionClick ,
19
22
ActionDoubleClick ,
26
29
ActionWait ,
27
30
)
28
31
from openai .types .responses .response_input_param import ComputerCallOutput , McpApprovalResponse
29
- from openai .types .responses .response_output_item import McpApprovalRequest , McpCall , McpListTools
32
+ from openai .types .responses .response_output_item import (
33
+ ImageGenerationCall ,
34
+ LocalShellCall ,
35
+ McpApprovalRequest ,
36
+ McpListTools ,
37
+ )
30
38
from openai .types .responses .response_reasoning_item import ResponseReasoningItem
31
39
32
40
from .agent import Agent , ToolsToFinalOutputResult
61
69
FunctionTool ,
62
70
FunctionToolResult ,
63
71
HostedMCPTool ,
72
+ LocalShellCommandRequest ,
73
+ LocalShellTool ,
64
74
MCPToolApprovalRequest ,
65
75
Tool ,
66
76
)
@@ -129,12 +139,19 @@ class ToolRunMCPApprovalRequest:
129
139
mcp_tool : HostedMCPTool
130
140
131
141
142
+ @dataclass
143
+ class ToolRunLocalShellCall :
144
+ tool_call : LocalShellCall
145
+ local_shell_tool : LocalShellTool
146
+
147
+
132
148
@dataclass
133
149
class ProcessedResponse :
134
150
new_items : list [RunItem ]
135
151
handoffs : list [ToolRunHandoff ]
136
152
functions : list [ToolRunFunction ]
137
153
computer_actions : list [ToolRunComputerAction ]
154
+ local_shell_calls : list [ToolRunLocalShellCall ]
138
155
tools_used : list [str ] # Names of all tools used, including hosted tools
139
156
mcp_approval_requests : list [ToolRunMCPApprovalRequest ] # Only requests with callbacks
140
157
@@ -146,6 +163,7 @@ def has_tools_or_approvals_to_run(self) -> bool:
146
163
self .handoffs ,
147
164
self .functions ,
148
165
self .computer_actions ,
166
+ self .local_shell_calls ,
149
167
self .mcp_approval_requests ,
150
168
]
151
169
)
@@ -371,11 +389,15 @@ def process_model_response(
371
389
run_handoffs = []
372
390
functions = []
373
391
computer_actions = []
392
+ local_shell_calls = []
374
393
mcp_approval_requests = []
375
394
tools_used : list [str ] = []
376
395
handoff_map = {handoff .tool_name : handoff for handoff in handoffs }
377
396
function_map = {tool .name : tool for tool in all_tools if isinstance (tool , FunctionTool )}
378
397
computer_tool = next ((tool for tool in all_tools if isinstance (tool , ComputerTool )), None )
398
+ local_shell_tool = next (
399
+ (tool for tool in all_tools if isinstance (tool , LocalShellTool )), None
400
+ )
379
401
hosted_mcp_server_map = {
380
402
tool .tool_config ["server_label" ]: tool
381
403
for tool in all_tools
@@ -434,9 +456,29 @@ def process_model_response(
434
456
)
435
457
elif isinstance (output , McpListTools ):
436
458
items .append (MCPListToolsItem (raw_item = output , agent = agent ))
437
- elif isinstance (output , McpCall ):
459
+ elif isinstance (output , ImageGenerationCall ):
460
+ items .append (ToolCallItem (raw_item = output , agent = agent ))
461
+ tools_used .append ("image_generation" )
462
+ elif isinstance (output , ResponseCodeInterpreterToolCall ):
438
463
items .append (ToolCallItem (raw_item = output , agent = agent ))
439
- tools_used .append (output .name )
464
+ tools_used .append ("code_interpreter" )
465
+ elif isinstance (output , LocalShellCall ):
466
+ items .append (ToolCallItem (raw_item = output , agent = agent ))
467
+ tools_used .append ("local_shell" )
468
+ if not local_shell_tool :
469
+ _error_tracing .attach_error_to_current_span (
470
+ SpanError (
471
+ message = "Local shell tool not found" ,
472
+ data = {},
473
+ )
474
+ )
475
+ raise ModelBehaviorError (
476
+ "Model produced local shell call without a local shell tool."
477
+ )
478
+ local_shell_calls .append (
479
+ ToolRunLocalShellCall (tool_call = output , local_shell_tool = local_shell_tool )
480
+ )
481
+
440
482
elif not isinstance (output , ResponseFunctionToolCall ):
441
483
logger .warning (f"Unexpected output type, ignoring: { type (output )} " )
442
484
continue
@@ -478,6 +520,7 @@ def process_model_response(
478
520
handoffs = run_handoffs ,
479
521
functions = functions ,
480
522
computer_actions = computer_actions ,
523
+ local_shell_calls = local_shell_calls ,
481
524
tools_used = tools_used ,
482
525
mcp_approval_requests = mcp_approval_requests ,
483
526
)
@@ -552,6 +595,30 @@ async def run_single_tool(
552
595
for tool_run , result in zip (tool_runs , results )
553
596
]
554
597
598
+ @classmethod
599
+ async def execute_local_shell_calls (
600
+ cls ,
601
+ * ,
602
+ agent : Agent [TContext ],
603
+ calls : list [ToolRunLocalShellCall ],
604
+ context_wrapper : RunContextWrapper [TContext ],
605
+ hooks : RunHooks [TContext ],
606
+ config : RunConfig ,
607
+ ) -> list [RunItem ]:
608
+ results : list [RunItem ] = []
609
+ # Need to run these serially, because each call can affect the local shell state
610
+ for call in calls :
611
+ results .append (
612
+ await LocalShellAction .execute (
613
+ agent = agent ,
614
+ call = call ,
615
+ hooks = hooks ,
616
+ context_wrapper = context_wrapper ,
617
+ config = config ,
618
+ )
619
+ )
620
+ return results
621
+
555
622
@classmethod
556
623
async def execute_computer_actions (
557
624
cls ,
@@ -1021,3 +1088,54 @@ async def _get_screenshot_async(
1021
1088
await computer .wait ()
1022
1089
1023
1090
return await computer .screenshot ()
1091
+
1092
+
1093
+ class LocalShellAction :
1094
+ @classmethod
1095
+ async def execute (
1096
+ cls ,
1097
+ * ,
1098
+ agent : Agent [TContext ],
1099
+ call : ToolRunLocalShellCall ,
1100
+ hooks : RunHooks [TContext ],
1101
+ context_wrapper : RunContextWrapper [TContext ],
1102
+ config : RunConfig ,
1103
+ ) -> RunItem :
1104
+ await asyncio .gather (
1105
+ hooks .on_tool_start (context_wrapper , agent , call .local_shell_tool ),
1106
+ (
1107
+ agent .hooks .on_tool_start (context_wrapper , agent , call .local_shell_tool )
1108
+ if agent .hooks
1109
+ else _coro .noop_coroutine ()
1110
+ ),
1111
+ )
1112
+
1113
+ request = LocalShellCommandRequest (
1114
+ ctx_wrapper = context_wrapper ,
1115
+ data = call .tool_call ,
1116
+ )
1117
+ output = call .local_shell_tool .executor (request )
1118
+ if inspect .isawaitable (output ):
1119
+ result = await output
1120
+ else :
1121
+ result = output
1122
+
1123
+ await asyncio .gather (
1124
+ hooks .on_tool_end (context_wrapper , agent , call .local_shell_tool , result ),
1125
+ (
1126
+ agent .hooks .on_tool_end (context_wrapper , agent , call .local_shell_tool , result )
1127
+ if agent .hooks
1128
+ else _coro .noop_coroutine ()
1129
+ ),
1130
+ )
1131
+
1132
+ return ToolCallOutputItem (
1133
+ agent = agent ,
1134
+ output = output ,
1135
+ raw_item = {
1136
+ "type" : "local_shell_call_output" ,
1137
+ "id" : call .tool_call .call_id ,
1138
+ "output" : result ,
1139
+ # "id": "out" + call.tool_call.id, # TODO remove this, it should be optional
1140
+ },
1141
+ )
0 commit comments