Skip to content

Commit 702210f

Browse files
fix(ui): refuse a tool decision that arrives before its prompt
An approval sent before the agent had opened its permission prompt was reported as delivered, then overwritten when the prompt opened, so the run waited out its whole timeout and denied the tool. resolve_tool_confirmation now returns False when nothing is pending, as its docstring always said, so the /tool_decision endpoint answers 409 and the caller retries. This made "Gaia Agent Tests" fail on main whenever the test's answer won the race; the test now polls on a deadline and says plainly when no answer landed.
1 parent 6c3bb5c commit 702210f

3 files changed

Lines changed: 38 additions & 12 deletions

File tree

‎hub/agents/gaia/python/tests/test_server_query.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1089,15 +1089,19 @@ def test_a_gated_tool_runs_once_the_decision_arrives(built):
10891089
client, agents = built
10901090
run_id = str(uuid.uuid4())
10911091

1092+
answered = threading.Event()
1093+
10921094
def answer_when_asked():
10931095
# The decision endpoint 409s until the prompt is actually pending, so
10941096
# poll rather than sleep a guessed interval.
1095-
for _ in range(400):
1097+
deadline = time.monotonic() + 30
1098+
while time.monotonic() < deadline:
10961099
response = client.post(
10971100
f"/v1/gaia/query/{run_id}/tool_decision",
10981101
json={"decision": "allow"},
10991102
)
11001103
if response.status_code == 200:
1104+
answered.set()
11011105
return
11021106
time.sleep(0.01)
11031107

@@ -1116,6 +1120,7 @@ def arm(**kw):
11161120
)
11171121

11181122
assert response.status_code == 200, response.text
1123+
assert answered.is_set(), "the decision was never accepted while the prompt was open"
11191124
events = _events(response)
11201125
types = [e.get("type") for e in events]
11211126
assert "needs_confirmation" in types, f"the prompt must reach the client: {types}"

‎src/gaia/ui/sse_handler.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,12 +1232,14 @@ def resolve_tool_confirmation(
12321232
self._confirm_id,
12331233
)
12341234
return False
1235+
if self._confirm_event is None:
1236+
# Nothing is waiting: an answer stored now would be overwritten
1237+
# when the prompt opens, so report it undelivered and let the
1238+
# caller retry.
1239+
logger.warning("Dropped a tool decision: no confirmation is pending")
1240+
return False
12351241
if always and self._confirm_tool:
12361242
self.grant_call_for_session(self._confirm_tool, self._confirm_args)
1237-
if self._confirm_event is None:
1238-
# No pending confirmation — initialise state anyway so callers can
1239-
# inspect _confirm_result and _confirm_event after the call.
1240-
self._confirm_event = threading.Event()
12411243
self._confirm_result = (approved or always) and not timed_out
12421244
self._confirm_timed_out = timed_out
12431245
self._confirm_event.set()

‎tests/unit/chat/ui/test_sse_confirmation.py‎

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -197,13 +197,32 @@ def run_confirm():
197197

198198

199199
class TestResolveToolConfirmationNoPending:
200-
"""resolve_tool_confirmation with no pending request just sets the event."""
201-
202-
def test_no_pending_sets_event(self, handler):
203-
"""Calling resolve with no pending confirm just sets the event/result."""
204-
handler.resolve_tool_confirmation(approved=True)
205-
assert handler._confirm_result is True
206-
assert handler._confirm_event.is_set()
200+
"""A decision with nothing pending is refused, so the caller can retry."""
201+
202+
def test_no_pending_is_not_delivered(self, handler):
203+
assert handler.resolve_tool_confirmation(approved=True) is False
204+
assert handler._confirm_event is None
205+
206+
def test_an_early_answer_does_not_satisfy_a_later_prompt(self, handler):
207+
"""Reported as delivered, it was then overwritten when the prompt opened,
208+
and the run waited out its whole timeout."""
209+
assert handler.resolve_tool_confirmation(approved=True) is False
210+
211+
result = {}
212+
thread = threading.Thread(
213+
target=lambda: result.setdefault(
214+
"approved",
215+
handler.confirm_tool_execution("run_shell_command", {}, timeout=10),
216+
),
217+
daemon=True,
218+
)
219+
thread.start()
220+
deadline = time.monotonic() + 5
221+
while handler._confirm_event is None and time.monotonic() < deadline:
222+
time.sleep(0.01)
223+
assert handler.resolve_tool_confirmation(approved=True) is True
224+
thread.join(timeout=5)
225+
assert result == {"approved": True}
207226

208227

209228
# ===========================================================================

0 commit comments

Comments
 (0)