Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/agents/run_internal/tool_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -1091,7 +1091,12 @@ async def resolve_approval_status(
if decision_result.get("approve") is True:
context_wrapper.approve_tool(approval_item)
elif decision_result.get("approve") is False:
context_wrapper.reject_tool(approval_item)
reason = decision_result.get("reason")
rejection_message = reason if isinstance(reason, str) and reason else None
context_wrapper.reject_tool(
approval_item,
rejection_message=rejection_message,
)
approval_status = context_wrapper.get_approval_status(
tool_name,
call_id,
Expand Down
4 changes: 3 additions & 1 deletion tests/test_apply_patch_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,9 @@ async def test_apply_patch_tool_on_approval_callback_auto_rejects() -> None:

# Should return rejection output
assert isinstance(result, ToolCallOutputItem)
assert HITL_REJECTION_MSG in result.output
assert result.output == "Not allowed"
raw_item = cast(dict[str, Any], result.raw_item)
assert raw_item["output"] == "Not allowed"
assert len(editor.operations) == 0 # Should not have executed


Expand Down
47 changes: 46 additions & 1 deletion tests/test_custom_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
from openai.types.responses import ResponseCustomToolCall

from agents import Agent, CustomTool, RunConfig, RunContextWrapper
from agents.items import ToolCallOutputItem
from agents.items import ToolApprovalItem, ToolCallOutputItem
from agents.lifecycle import RunHooks
from agents.run_internal.run_steps import ToolRunCustom
from agents.run_internal.tool_actions import CustomToolAction
from agents.tool import CustomToolOnApprovalFunctionResult
from agents.tool_context import ToolContext


Expand Down Expand Up @@ -47,3 +48,47 @@ async def invoke(ctx: ToolContext[Any], raw_input: str) -> str:
"call_id": "call_custom",
"output": "HELLO",
}


@pytest.mark.asyncio
async def test_custom_tool_on_approval_callback_auto_rejects_with_reason() -> None:
async def invoke(_ctx: ToolContext[Any], _raw_input: str) -> str:
raise AssertionError("rejected custom tool should not execute")

async def on_approval(
_context: RunContextWrapper[Any], _approval_item: ToolApprovalItem
) -> CustomToolOnApprovalFunctionResult:
return {"approve": False, "reason": "Not allowed"}

tool = CustomTool(
name="raw_editor",
description="Edit raw text.",
on_invoke_tool=invoke,
format={"type": "text"},
needs_approval=True,
on_approval=on_approval,
)
agent = Agent(name="custom-agent", tools=[tool])
tool_call = ResponseCustomToolCall(
type="custom_tool_call",
name="raw_editor",
call_id="call_custom",
input="hello",
)

result = await CustomToolAction.execute(
agent=agent,
call=ToolRunCustom(tool_call=tool_call, custom_tool=tool),
hooks=RunHooks[Any](),
context_wrapper=RunContextWrapper(context=None),
config=RunConfig(),
)

assert isinstance(result, ToolCallOutputItem)
assert result.output == "Not allowed"
raw_item = cast(dict[str, Any], result.raw_item)
assert raw_item == {
"type": "custom_tool_call_output",
"call_id": "call_custom",
"output": "Not allowed",
}
36 changes: 35 additions & 1 deletion tests/test_shell_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
)
from agents.items import ToolApprovalItem, ToolCallOutputItem
from agents.run_internal.run_loop import ShellAction, ToolRunShellCall, execute_shell_calls
from agents.tool import ShellOnApprovalFunctionResult

from .testing_processor import SPAN_PROCESSOR_TESTING
from .utils.hitl import (
Expand Down Expand Up @@ -776,4 +777,37 @@ async def test_shell_tool_on_approval_callback_auto_rejects() -> None:

# Should return rejection output
assert isinstance(result, ToolCallOutputItem)
assert HITL_REJECTION_MSG in result.output
assert result.output == "Not allowed"
raw_item = cast(dict[str, Any], result.raw_item)
assert raw_item["output"][0]["stderr"] == "Not allowed"


@pytest.mark.asyncio
async def test_shell_tool_on_approval_empty_reason_uses_default_rejection() -> None:
"""Test that empty rejection reasons do not suppress the default message."""

async def on_approval(
_context: RunContextWrapper[Any], _approval_item: ToolApprovalItem
) -> ShellOnApprovalFunctionResult:
return {"approve": False, "reason": ""}

shell_tool = ShellTool(
executor=lambda request: "output",
needs_approval=require_approval,
on_approval=on_approval,
)

tool_run = ToolRunShellCall(tool_call=_shell_call(), shell_tool=shell_tool)
agent = Agent(name="shell-agent", tools=[shell_tool])
context_wrapper: RunContextWrapper[Any] = make_context_wrapper()

result = await ShellAction.execute(
agent=agent,
call=tool_run,
hooks=RunHooks[Any](),
context_wrapper=context_wrapper,
config=RunConfig(),
)

assert isinstance(result, ToolCallOutputItem)
assert result.output == HITL_REJECTION_MSG
Loading