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
30 changes: 23 additions & 7 deletions src/agents/realtime/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,19 +668,25 @@ async def _maybe_request_tool_approval(
agent,
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

approval_status = self._context_wrapper.get_approval_status(
function_tool.name,
tool_call.call_id,
existing_pending=approval_item,
tool_lookup_key=tool_lookup_key,
)
if approval_status is None:
needs_approval = await self._function_needs_approval(function_tool, tool_call)
if self._closing or self._closed:
return None
approval_status = self._context_wrapper.get_approval_status(
function_tool.name,
tool_call.call_id,
existing_pending=approval_item,
tool_lookup_key=tool_lookup_key,
)
if approval_status is None and not needs_approval:
return True

if approval_status is True:
return True
if approval_status is False:
Expand All @@ -694,6 +700,16 @@ async def _maybe_request_tool_approval(
)
if self._closing or self._closed:
return None
approval_status = self._context_wrapper.get_approval_status(
function_tool.name,
tool_call.call_id,
existing_pending=approval_item,
tool_lookup_key=tool_lookup_key,
)
if approval_status is True:
return True
if approval_status is False:
return False
if rejected_message is not None:
return self._build_realtime_tool_output(
tool=function_tool,
Expand Down
156 changes: 89 additions & 67 deletions src/agents/run_internal/tool_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,11 +454,23 @@ 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
approval_status = context_wrapper.get_approval_status(
shell_tool.name, shell_call.call_id
)
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,
)
approval_status = context_wrapper.get_approval_status(
shell_tool.name, shell_call.call_id
)
else:
needs_approval_result = False

if needs_approval_result:
if approval_status is None and needs_approval_result:
approval_status, approval_item = await resolve_approval_status(
tool_name=shell_tool.name,
call_id=shell_call.call_id,
Expand All @@ -468,24 +480,24 @@ async def _run_call(span: Any | None) -> RunItem:
on_approval=shell_tool.on_approval,
)

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:
if approval_status is None:
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,
)

await asyncio.gather(
hooks.on_tool_start(context_wrapper, agent, shell_tool),
(
Expand Down Expand Up @@ -649,11 +661,16 @@ 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
)
approval_status = context_wrapper.get_approval_status(custom_tool.name, call_id)
if approval_status is None:
needs_approval_result = await evaluate_needs_approval_setting(
custom_tool.runtime_needs_approval(), context_wrapper, tool_input, call_id
)
approval_status = context_wrapper.get_approval_status(custom_tool.name, call_id)
else:
needs_approval_result = False

if needs_approval_result:
if approval_status is None and needs_approval_result:
approval_status, approval_item = await resolve_approval_status(
tool_name=custom_tool.name,
call_id=call_id,
Expand All @@ -663,27 +680,27 @@ async def _run_call(span: Any | None) -> RunItem:
on_approval=custom_tool.runtime_on_approval(),
)

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,
if approval_status is None:
return approval_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,
raw_item=cls._raw_tool_output_item(
call_id,
rejection_message,
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
tool_call=call.tool_call,
),
)

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

approval_status = context_wrapper.get_approval_status(apply_patch_tool.name, call_id)
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:
if approval_status is None:
for operation in operations:
needs_approval_result = await evaluate_needs_approval_setting(
apply_patch_tool.needs_approval, context_wrapper, operation, call_id
)
approval_status = context_wrapper.get_approval_status(
apply_patch_tool.name, call_id
)
if approval_status is not None or needs_approval_result:
break

if approval_status is None and needs_approval_result:
approval_status, approval_item = await resolve_approval_status(
tool_name=apply_patch_tool.name,
call_id=call_id,
Expand All @@ -848,25 +870,25 @@ async def _run_call(span: Any | None) -> RunItem:
on_approval=apply_patch_tool.on_approval,
)

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:
if approval_status is None:
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,
)

await asyncio.gather(
hooks.on_tool_start(context_wrapper, agent, apply_patch_tool),
(
Expand Down
33 changes: 24 additions & 9 deletions src/agents/run_internal/tool_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -1724,14 +1724,6 @@ async def _maybe_execute_tool_approval(
raw_tool_call: ResponseFunctionToolCall,
span_fn: Span[Any],
) -> Any | None:
needs_approval_result = await function_needs_approval(
func_tool,
self.context_wrapper,
tool_call,
)
if not needs_approval_result:
return None

tool_namespace = get_tool_call_namespace(raw_tool_call)
if tool_namespace is None and is_deferred_top_level_function_tool(func_tool):
tool_namespace = func_tool.name
Expand All @@ -1744,6 +1736,21 @@ async def _maybe_execute_tool_approval(
tool_namespace=tool_namespace,
tool_lookup_key=tool_lookup_key,
)
if approval_status is None:
needs_approval_result = await function_needs_approval(
func_tool,
self.context_wrapper,
tool_call,
)
Comment thread
seratch marked this conversation as resolved.
approval_status = self.context_wrapper.get_approval_status(
func_tool.name,
tool_call.call_id,
tool_namespace=tool_namespace,
tool_lookup_key=tool_lookup_key,
)
if approval_status is None and not needs_approval_result:
return None

if approval_status is None:
if self._should_run_pre_approval_tool_input_guardrails():
tool_context_namespace = get_tool_call_namespace(raw_tool_call)
Expand All @@ -1763,7 +1770,13 @@ async def _maybe_execute_tool_approval(
agent=self.public_agent,
tool_input_guardrail_results=self.tool_input_guardrail_results,
)
if rejected_message is not None:
approval_status = self.context_wrapper.get_approval_status(
func_tool.name,
tool_call.call_id,
tool_namespace=tool_namespace,
tool_lookup_key=tool_lookup_key,
Comment thread
seratch marked this conversation as resolved.
)
if approval_status is None and rejected_message is not None:
return FunctionToolResult(
tool=func_tool,
output=rejected_message,
Expand All @@ -1776,6 +1789,8 @@ async def _maybe_execute_tool_approval(
tool_origin=get_function_tool_origin(func_tool),
),
)

if approval_status is None:
approval_item = ToolApprovalItem(
agent=self.public_agent,
raw_item=raw_tool_call,
Expand Down
39 changes: 24 additions & 15 deletions src/agents/run_internal/tool_planning.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,20 @@ async def _collect_runs_by_approval(
if output_exists_checker and output_exists_checker(call_id):
continue

needs_approval = True
if approval_status is None and needs_approval_checker:
try:
needs_approval = await needs_approval_checker(run)
except UserError:
raise
except Exception:
needs_approval = True
approval_status = context_wrapper.get_approval_status(
tool_name,
call_id,
existing_pending=existing_pending,
)

if approval_status is False:
rejection = rejection_builder(run, call_id)
if inspect.isawaitable(rejection):
Expand All @@ -417,15 +431,6 @@ async def _collect_runs_by_approval(
approved_runs.append(run)
continue

needs_approval = True
if needs_approval_checker:
try:
needs_approval = await needs_approval_checker(run)
except UserError:
raise
except Exception:
needs_approval = True

if not needs_approval:
approved_runs.append(run)
continue
Expand Down Expand Up @@ -517,6 +522,16 @@ async def _select_function_tool_runs_for_resume(
existing_pending=approval_items_by_call_id.get(call_id),
)

requires_approval = True
if approval_status is None:
requires_approval = await needs_approval_checker(run)
approval_status = context_wrapper.get_approval_status(
run.function_tool.name,
call_id,
tool_namespace=get_tool_call_namespace(run.tool_call),
existing_pending=approval_items_by_call_id.get(call_id),
)

if approval_status is False:
await record_rejection(call_id, run.tool_call, run.function_tool)
continue
Expand All @@ -525,12 +540,6 @@ async def _select_function_tool_runs_for_resume(
selected.append(run)
continue

# Only invoke needs_approval_checker when the approval state is unresolved;
# for explicit approve/reject decisions the checker's result is unused, and
# invoking it eagerly risks user-side effects (or exceptions that swallow
# rejections) on calls whose outcome is already determined.
requires_approval = await needs_approval_checker(run)

if not requires_approval:
selected.append(run)
continue
Expand Down
Loading