Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Gate hidden approvals on explicit approval responses
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
  • Loading branch information
eavanvalkenburg and Copilot committed Jun 10, 2026
commit b842ac30fce4a6137e2f15e6b6bb1e884e22070a
12 changes: 9 additions & 3 deletions python/packages/core/agent_framework/_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -2255,9 +2255,15 @@ async def _process_function_requests(
from ._types import Message

if prepped_messages is not None:
pending_auto_approval_responses = _pop_auto_approvable_approval_responses(invocation_session)
if pending_auto_approval_responses:
prepped_messages.append(Message(role="user", contents=pending_auto_approval_responses))
has_explicit_approval_response = any(
content.type == "function_approval_response"
for message in prepped_messages
for content in message.contents
)
if has_explicit_approval_response:
Comment thread
eavanvalkenburg marked this conversation as resolved.
Resolved
pending_auto_approval_responses = _pop_auto_approvable_approval_responses(invocation_session)
if pending_auto_approval_responses:
prepped_messages.append(Message(role="user", contents=pending_auto_approval_responses))
fcc_todo = _collect_approval_responses(prepped_messages)
if not fcc_todo:
fcc_todo = {}
Expand Down
51 changes: 51 additions & 0 deletions python/packages/core/tests/core/test_harness_tool_approval.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,57 @@ def risky_write() -> str:
assert risky_calls == 1


async def test_hidden_mixed_batch_requests_do_not_replay_on_unrelated_turn(
chat_client_base: SupportsChatGetResponse,
) -> None:
"""Stored hidden approvals should only replay when an approval response resumes the flow."""
safe_calls = 0
risky_calls = 0

@tool(name="safe_lookup", approval_mode="never_require")
def safe_lookup() -> str:
nonlocal safe_calls
safe_calls += 1
return "safe"

@tool(name="risky_update", approval_mode="always_require")
def risky_update() -> str:
nonlocal risky_calls
risky_calls += 1
return "risky"

agent = Agent(client=chat_client_base, tools=[safe_lookup, risky_update])
session = AgentSession(session_id="stale-hidden-session")
chat_client_base.run_responses = [
ChatResponse(
messages=Message(
role="assistant",
contents=[
Content.from_function_call(call_id="call_safe", name="safe_lookup", arguments="{}"),
Content.from_function_call(call_id="call_risky", name="risky_update", arguments="{}"),
],
)
)
]

first_response = await agent.run("lookup and update", session=session)
request = _approval_requests(first_response.messages)[0]

chat_client_base.run_responses = [ChatResponse(messages=Message(role="assistant", contents=["unrelated"]))]
unrelated_response = await agent.run("never mind, answer something else", session=session)

assert unrelated_response.text == "unrelated"
assert safe_calls == 0
assert risky_calls == 0

chat_client_base.run_responses = [ChatResponse(messages=Message(role="assistant", contents=["done"]))]
final_response = await agent.run(request.to_function_approval_response(approved=True), session=session)

assert final_response.text == "done"
assert safe_calls == 1
assert risky_calls == 1


async def test_tool_approval_middleware_queues_multiple_approval_requests(
chat_client_base: SupportsChatGetResponse,
) -> None:
Expand Down