Skip to content

Commit 8664271

Browse files
authored
Add safety check handling for ComputerTool (#923)
### Summary - handle safety checks when using computer tools - expose new `on_safety_check` callback and data structure - return acknowledged checks in computer output - test acknowledging safety checks ### Test plan - `make format` - `make lint` - `make mypy` - `make tests` Resolves #843 ------ https://chatgpt.com/codex/tasks/task_i_684f207b8b588321a33642a2a9c96a1a
1 parent fcafae1 commit 8664271

File tree

3 files changed

+98
-1
lines changed

3 files changed

+98
-1
lines changed

src/agents/_run_impl.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
ActionType,
2929
ActionWait,
3030
)
31+
from openai.types.responses.response_input_item_param import (
32+
ComputerCallOutputAcknowledgedSafetyCheck,
33+
)
3134
from openai.types.responses.response_input_param import ComputerCallOutput, McpApprovalResponse
3235
from openai.types.responses.response_output_item import (
3336
ImageGenerationCall,
@@ -67,6 +70,7 @@
6770
from .stream_events import RunItemStreamEvent, StreamEvent
6871
from .tool import (
6972
ComputerTool,
73+
ComputerToolSafetyCheckData,
7074
FunctionTool,
7175
FunctionToolResult,
7276
HostedMCPTool,
@@ -638,13 +642,37 @@ async def execute_computer_actions(
638642
results: list[RunItem] = []
639643
# Need to run these serially, because each action can affect the computer state
640644
for action in actions:
645+
acknowledged: list[ComputerCallOutputAcknowledgedSafetyCheck] | None = None
646+
if action.tool_call.pending_safety_checks and action.computer_tool.on_safety_check:
647+
acknowledged = []
648+
for check in action.tool_call.pending_safety_checks:
649+
data = ComputerToolSafetyCheckData(
650+
ctx_wrapper=context_wrapper,
651+
agent=agent,
652+
tool_call=action.tool_call,
653+
safety_check=check,
654+
)
655+
maybe = action.computer_tool.on_safety_check(data)
656+
ack = await maybe if inspect.isawaitable(maybe) else maybe
657+
if ack:
658+
acknowledged.append(
659+
ComputerCallOutputAcknowledgedSafetyCheck(
660+
id=check.id,
661+
code=check.code,
662+
message=check.message,
663+
)
664+
)
665+
else:
666+
raise UserError("Computer tool safety check was not acknowledged")
667+
641668
results.append(
642669
await ComputerAction.execute(
643670
agent=agent,
644671
action=action,
645672
hooks=hooks,
646673
context_wrapper=context_wrapper,
647674
config=config,
675+
acknowledged_safety_checks=acknowledged,
648676
)
649677
)
650678

@@ -998,6 +1026,7 @@ async def execute(
9981026
hooks: RunHooks[TContext],
9991027
context_wrapper: RunContextWrapper[TContext],
10001028
config: RunConfig,
1029+
acknowledged_safety_checks: list[ComputerCallOutputAcknowledgedSafetyCheck] | None = None,
10011030
) -> RunItem:
10021031
output_func = (
10031032
cls._get_screenshot_async(action.computer_tool.computer, action.tool_call)
@@ -1036,6 +1065,7 @@ async def execute(
10361065
"image_url": image_url,
10371066
},
10381067
type="computer_call_output",
1068+
acknowledged_safety_checks=acknowledged_safety_checks,
10391069
),
10401070
)
10411071

src/agents/tool.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
from typing import TYPE_CHECKING, Any, Callable, Literal, Union, overload
88

99
from openai.types.responses.file_search_tool_param import Filters, RankingOptions
10+
from openai.types.responses.response_computer_tool_call import (
11+
PendingSafetyCheck,
12+
ResponseComputerToolCall,
13+
)
1014
from openai.types.responses.response_output_item import LocalShellCall, McpApprovalRequest
1115
from openai.types.responses.tool_param import CodeInterpreter, ImageGeneration, Mcp
1216
from openai.types.responses.web_search_tool_param import UserLocation
@@ -142,11 +146,31 @@ class ComputerTool:
142146
as well as implements the computer actions like click, screenshot, etc.
143147
"""
144148

149+
on_safety_check: Callable[[ComputerToolSafetyCheckData], MaybeAwaitable[bool]] | None = None
150+
"""Optional callback to acknowledge computer tool safety checks."""
151+
145152
@property
146153
def name(self):
147154
return "computer_use_preview"
148155

149156

157+
@dataclass
158+
class ComputerToolSafetyCheckData:
159+
"""Information about a computer tool safety check."""
160+
161+
ctx_wrapper: RunContextWrapper[Any]
162+
"""The run context."""
163+
164+
agent: Agent[Any]
165+
"""The agent performing the computer action."""
166+
167+
tool_call: ResponseComputerToolCall
168+
"""The computer tool call."""
169+
170+
safety_check: PendingSafetyCheck
171+
"""The pending safety check to acknowledge."""
172+
173+
150174
@dataclass
151175
class MCPToolApprovalRequest:
152176
"""A request to approve a tool call."""

tests/test_computer_action.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
ActionScroll,
1919
ActionType,
2020
ActionWait,
21+
PendingSafetyCheck,
2122
ResponseComputerToolCall,
2223
)
2324

@@ -31,8 +32,9 @@
3132
RunContextWrapper,
3233
RunHooks,
3334
)
34-
from agents._run_impl import ComputerAction, ToolRunComputerAction
35+
from agents._run_impl import ComputerAction, RunImpl, ToolRunComputerAction
3536
from agents.items import ToolCallOutputItem
37+
from agents.tool import ComputerToolSafetyCheckData
3638

3739

3840
class LoggingComputer(Computer):
@@ -309,3 +311,44 @@ async def test_execute_invokes_hooks_and_returns_tool_call_output() -> None:
309311
assert raw["output"]["type"] == "computer_screenshot"
310312
assert "image_url" in raw["output"]
311313
assert raw["output"]["image_url"].endswith("xyz")
314+
315+
316+
@pytest.mark.asyncio
317+
async def test_pending_safety_check_acknowledged() -> None:
318+
"""Safety checks should be acknowledged via the callback."""
319+
320+
computer = LoggingComputer(screenshot_return="img")
321+
called: list[ComputerToolSafetyCheckData] = []
322+
323+
def on_sc(data: ComputerToolSafetyCheckData) -> bool:
324+
called.append(data)
325+
return True
326+
327+
tool = ComputerTool(computer=computer, on_safety_check=on_sc)
328+
safety = PendingSafetyCheck(id="sc", code="c", message="m")
329+
tool_call = ResponseComputerToolCall(
330+
id="t1",
331+
type="computer_call",
332+
action=ActionClick(type="click", x=1, y=1, button="left"),
333+
call_id="t1",
334+
pending_safety_checks=[safety],
335+
status="completed",
336+
)
337+
run_action = ToolRunComputerAction(tool_call=tool_call, computer_tool=tool)
338+
agent = Agent(name="a", tools=[tool])
339+
ctx = RunContextWrapper(context=None)
340+
341+
results = await RunImpl.execute_computer_actions(
342+
agent=agent,
343+
actions=[run_action],
344+
hooks=RunHooks[Any](),
345+
context_wrapper=ctx,
346+
config=RunConfig(),
347+
)
348+
349+
assert len(results) == 1
350+
raw = results[0].raw_item
351+
assert isinstance(raw, dict)
352+
assert raw.get("acknowledged_safety_checks") == [{"id": "sc", "code": "c", "message": "m"}]
353+
assert len(called) == 1
354+
assert called[0].safety_check.id == "sc"

0 commit comments

Comments
 (0)