Skip to content
Draft
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
14 changes: 8 additions & 6 deletions src/agents/realtime/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,12 +607,8 @@ async def _maybe_request_tool_approval(
tool_lookup_key=tool_lookup_key,
)

needs_approval = await self._function_needs_approval(function_tool, tool_call)
if self._closing or self._closed:
return None
if not needs_approval:
return True

# Resolved approve/reject decisions are authoritative; do not re-await
# needs_approval for a call whose status is already stored.
approval_status = self._context_wrapper.get_approval_status(
function_tool.name,
tool_call.call_id,
Expand All @@ -624,6 +620,12 @@ async def _maybe_request_tool_approval(
if approval_status is False:
return False

needs_approval = await self._function_needs_approval(function_tool, tool_call)
if self._closing or self._closed:
return None
if not needs_approval:
return True
Comment on lines +623 to +627

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recheck sticky decisions after awaiting the approval checker

With the default async_tool_calls=True, two calls can evaluate approval concurrently: call B can read None here and suspend in a slow checker while call A becomes pending and is rejected with always=True. If B's checker then returns False, this path immediately returns True and executes B despite the newly stored sticky rejection. Re-read get_approval_status after the await, before either bypassing approval or creating another pending request, so concurrent decisions remain authoritative.

AGENTS.md reference: AGENTS.md:L106-L106

Useful? React with 👍 / 👎.


if self._pre_approval_tool_input_guardrails_enabled():
rejected_message = await self._run_tool_input_guardrails(
tool=function_tool,
Expand Down
215 changes: 140 additions & 75 deletions src/agents/run_internal/tool_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,37 +454,59 @@ async def _run_call(span: Any | None) -> RunItem:
dataclasses.asdict(shell_call.action)
)

needs_approval_result = await evaluate_needs_approval_setting(
shell_tool.needs_approval, context_wrapper, shell_call.action, shell_call.call_id
# Prefer stored approve/reject status over re-evaluating needs_approval.
approval_status = context_wrapper.get_approval_status(
shell_tool.name, shell_call.call_id
)

if needs_approval_result:
approval_status, approval_item = await resolve_approval_status(
if approval_status is False:
rejection_message = await resolve_approval_rejection_message(
context_wrapper=context_wrapper,
run_config=config,
tool_type="shell",
tool_name=shell_tool.name,
call_id=shell_call.call_id,
raw_item=call.tool_call,
agent=agent,
context_wrapper=context_wrapper,
on_approval=shell_tool.on_approval,
)
return shell_rejection_item(
agent,
shell_call.call_id,
tool_call=call.tool_call,
rejection_message=rejection_message,
)

if approval_status is False:
rejection_message = await resolve_approval_rejection_message(
context_wrapper=context_wrapper,
run_config=config,
tool_type="shell",
if approval_status is None:
needs_approval_result = await evaluate_needs_approval_setting(
shell_tool.needs_approval,
context_wrapper,
shell_call.action,
shell_call.call_id,
)
if needs_approval_result:
approval_status, approval_item = await resolve_approval_status(
tool_name=shell_tool.name,
call_id=shell_call.call_id,
)
return shell_rejection_item(
agent,
shell_call.call_id,
tool_call=call.tool_call,
rejection_message=rejection_message,
raw_item=call.tool_call,
agent=agent,
context_wrapper=context_wrapper,
on_approval=shell_tool.on_approval,
)

if approval_status is not True:
return approval_item
if approval_status is False:
rejection_message = await resolve_approval_rejection_message(
context_wrapper=context_wrapper,
run_config=config,
tool_type="shell",
tool_name=shell_tool.name,
call_id=shell_call.call_id,
)
return shell_rejection_item(
agent,
shell_call.call_id,
tool_call=call.tool_call,
rejection_message=rejection_message,
)

if approval_status is not True:
return approval_item

await asyncio.gather(
hooks.on_tool_start(context_wrapper, agent, shell_tool),
Expand Down Expand Up @@ -649,41 +671,65 @@ async def _run_call(span: Any | None) -> RunItem:
if span and config.trace_include_sensitive_data:
span.span_data.input = tool_input

needs_approval_result = await evaluate_needs_approval_setting(
custom_tool.runtime_needs_approval(), context_wrapper, tool_input, call_id
)

if needs_approval_result:
approval_status, approval_item = await resolve_approval_status(
# Prefer stored approve/reject status over re-evaluating needs_approval.
approval_status = context_wrapper.get_approval_status(custom_tool.name, call_id)
if approval_status is False:
rejection_message = await resolve_approval_rejection_message(
context_wrapper=context_wrapper,
run_config=config,
tool_type="custom",
tool_name=custom_tool.name,
call_id=call_id,
raw_item=call.tool_call,
agent=agent,
context_wrapper=context_wrapper,
on_approval=custom_tool.runtime_on_approval(),
)
return cls._tool_output_item(
agent,
call_id,
rejection_message,
raw_item=cls._raw_tool_output_item(
call_id,
rejection_message,
tool_call=call.tool_call,
),
)

if approval_status is False:
rejection_message = await resolve_approval_rejection_message(
context_wrapper=context_wrapper,
run_config=config,
tool_type="custom",
if approval_status is None:
needs_approval_result = await evaluate_needs_approval_setting(
custom_tool.runtime_needs_approval(),
context_wrapper,
tool_input,
call_id,
)
if needs_approval_result:
approval_status, approval_item = await resolve_approval_status(
tool_name=custom_tool.name,
call_id=call_id,
raw_item=call.tool_call,
agent=agent,
context_wrapper=context_wrapper,
on_approval=custom_tool.runtime_on_approval(),
)
return cls._tool_output_item(
agent,
call_id,
rejection_message,
raw_item=cls._raw_tool_output_item(

if approval_status is False:
rejection_message = await resolve_approval_rejection_message(
context_wrapper=context_wrapper,
run_config=config,
tool_type="custom",
tool_name=custom_tool.name,
call_id=call_id,
)
return cls._tool_output_item(
agent,
call_id,
rejection_message,
tool_call=call.tool_call,
),
)
raw_item=cls._raw_tool_output_item(
call_id,
rejection_message,
tool_call=call.tool_call,
),
)

if approval_status is not True:
return approval_item
if approval_status is not True:
return approval_item

await asyncio.gather(
hooks.on_tool_start(tool_context, agent, custom_tool),
Expand Down Expand Up @@ -830,42 +876,61 @@ async def _run_call(span: Any | None) -> RunItem:
]
)

needs_approval_result = False
for operation in operations:
if await evaluate_needs_approval_setting(
apply_patch_tool.needs_approval, context_wrapper, operation, call_id
):
needs_approval_result = True
break

if needs_approval_result:
approval_status, approval_item = await resolve_approval_status(
# Prefer stored approve/reject status over re-evaluating needs_approval.
approval_status = context_wrapper.get_approval_status(apply_patch_tool.name, call_id)
if approval_status is False:
rejection_message = await resolve_approval_rejection_message(
context_wrapper=context_wrapper,
run_config=config,
tool_type="apply_patch",
tool_name=apply_patch_tool.name,
call_id=call_id,
raw_item=call.tool_call,
agent=agent,
context_wrapper=context_wrapper,
on_approval=apply_patch_tool.on_approval,
)
return apply_patch_rejection_item(
agent,
call_id,
tool_call=call.tool_call,
output_type="apply_patch_call_output",
rejection_message=rejection_message,
)

if approval_status is False:
rejection_message = await resolve_approval_rejection_message(
context_wrapper=context_wrapper,
run_config=config,
tool_type="apply_patch",
if approval_status is None:
needs_approval_result = False
for operation in operations:
if await evaluate_needs_approval_setting(
apply_patch_tool.needs_approval, context_wrapper, operation, call_id
):
needs_approval_result = True
break

if needs_approval_result:
approval_status, approval_item = await resolve_approval_status(
tool_name=apply_patch_tool.name,
call_id=call_id,
raw_item=call.tool_call,
agent=agent,
context_wrapper=context_wrapper,
on_approval=apply_patch_tool.on_approval,
)
return apply_patch_rejection_item(
agent,
call_id,
tool_call=call.tool_call,
output_type="apply_patch_call_output",
rejection_message=rejection_message,
)

if approval_status is not True:
return approval_item
if approval_status is False:
rejection_message = await resolve_approval_rejection_message(
context_wrapper=context_wrapper,
run_config=config,
tool_type="apply_patch",
tool_name=apply_patch_tool.name,
call_id=call_id,
)
return apply_patch_rejection_item(
agent,
call_id,
tool_call=call.tool_call,
output_type="apply_patch_call_output",
rejection_message=rejection_message,
)

if approval_status is not True:
return approval_item

await asyncio.gather(
hooks.on_tool_start(context_wrapper, agent, apply_patch_tool),
Expand Down
Loading