1414 ResponseFunctionWebSearch ,
1515 ResponseOutputMessage ,
1616)
17+ from openai .types .responses .response_code_interpreter_tool_call import (
18+ ResponseCodeInterpreterToolCall ,
19+ )
1720from openai .types .responses .response_computer_tool_call import (
1821 ActionClick ,
1922 ActionDoubleClick ,
2629 ActionWait ,
2730)
2831from 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+ )
3038from openai .types .responses .response_reasoning_item import ResponseReasoningItem
3139
3240from .agent import Agent , ToolsToFinalOutputResult
6169 FunctionTool ,
6270 FunctionToolResult ,
6371 HostedMCPTool ,
72+ LocalShellCommandRequest ,
73+ LocalShellTool ,
6474 MCPToolApprovalRequest ,
6575 Tool ,
6676)
@@ -129,12 +139,19 @@ class ToolRunMCPApprovalRequest:
129139 mcp_tool : HostedMCPTool
130140
131141
142+ @dataclass
143+ class ToolRunLocalShellCall :
144+ tool_call : LocalShellCall
145+ local_shell_tool : LocalShellTool
146+
147+
132148@dataclass
133149class ProcessedResponse :
134150 new_items : list [RunItem ]
135151 handoffs : list [ToolRunHandoff ]
136152 functions : list [ToolRunFunction ]
137153 computer_actions : list [ToolRunComputerAction ]
154+ local_shell_calls : list [ToolRunLocalShellCall ]
138155 tools_used : list [str ] # Names of all tools used, including hosted tools
139156 mcp_approval_requests : list [ToolRunMCPApprovalRequest ] # Only requests with callbacks
140157
@@ -146,6 +163,7 @@ def has_tools_or_approvals_to_run(self) -> bool:
146163 self .handoffs ,
147164 self .functions ,
148165 self .computer_actions ,
166+ self .local_shell_calls ,
149167 self .mcp_approval_requests ,
150168 ]
151169 )
@@ -371,11 +389,15 @@ def process_model_response(
371389 run_handoffs = []
372390 functions = []
373391 computer_actions = []
392+ local_shell_calls = []
374393 mcp_approval_requests = []
375394 tools_used : list [str ] = []
376395 handoff_map = {handoff .tool_name : handoff for handoff in handoffs }
377396 function_map = {tool .name : tool for tool in all_tools if isinstance (tool , FunctionTool )}
378397 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+ )
379401 hosted_mcp_server_map = {
380402 tool .tool_config ["server_label" ]: tool
381403 for tool in all_tools
@@ -434,9 +456,29 @@ def process_model_response(
434456 )
435457 elif isinstance (output , McpListTools ):
436458 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 ):
438463 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+
440482 elif not isinstance (output , ResponseFunctionToolCall ):
441483 logger .warning (f"Unexpected output type, ignoring: { type (output )} " )
442484 continue
@@ -478,6 +520,7 @@ def process_model_response(
478520 handoffs = run_handoffs ,
479521 functions = functions ,
480522 computer_actions = computer_actions ,
523+ local_shell_calls = local_shell_calls ,
481524 tools_used = tools_used ,
482525 mcp_approval_requests = mcp_approval_requests ,
483526 )
@@ -552,6 +595,30 @@ async def run_single_tool(
552595 for tool_run , result in zip (tool_runs , results )
553596 ]
554597
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+
555622 @classmethod
556623 async def execute_computer_actions (
557624 cls ,
@@ -1021,3 +1088,54 @@ async def _get_screenshot_async(
10211088 await computer .wait ()
10221089
10231090 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