fix(code): simplify Auto fallback copy (#5670)

Auto fallback prompts now explain that review is needed without exposing
internal denial and availability counters.

---

Threshold reasons and counters remain available at `DEBUG`. The
transcript and approval description use plain language while preserving
the Switch-to-Manual flow and shell allow-list guard.

Made by [Open
SWE](https://openswe.vercel.app/agents/434260c2-4bed-5449-a0b4-95de592be3ec)

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Mason Daugherty
2026-08-21 12:48:01 -04:00
committed by GitHub
parent c961e31deb
commit 42afcded97
6 changed files with 78 additions and 20 deletions
+12 -5
View File
@@ -9213,7 +9213,7 @@ class DeepAgentsApp(App):
is_auto_fallback = any(
isinstance(request.get("description"), str)
and request["description"].startswith("Auto human fallback ")
and request["description"].startswith("Auto human fallback")
for request in action_requests or []
)
if settings.shell_allow_list and action_requests and not is_auto_fallback:
@@ -20718,11 +20718,18 @@ class DeepAgentsApp(App):
text = f"Auto fell back to Manual: {reason}"
self.notify(text, severity="warning", timeout=10, markup=False)
else:
if logger.isEnabledFor(logging.DEBUG):
logger.debug(
"Auto human fallback reason=%s consecutive_denials=%s "
"consecutive_unavailable=%s total_denials=%s",
reason,
event.get("consecutive_denials", 0),
event.get("consecutive_unavailable", 0),
event.get("total_denials", 0),
)
text = (
"Auto fallback: human approval required "
f"(denials {event.get('consecutive_denials', 0)}, "
f"unavailable {event.get('consecutive_unavailable', 0)}, "
f"total {event.get('total_denials', 0)})."
"Auto couldn't confidently approve this action, so it needs your "
"review. Auto will continue afterward."
)
else:
text = f"Auto warning: {reason}"
+2 -11
View File
@@ -2979,21 +2979,14 @@ class AutoModeHITLMiddleware(HumanInTheLoopMiddleware[AutoModeState, Any, Any]):
runtime: object,
*,
fallback: bool,
counters: AutoModeCounters | None,
fallback_reason: str | None = None,
) -> tuple[ActionRequest, ReviewConfig]:
config = self.interrupt_on[tool_call["name"]]
action, review = self._create_action_and_config(
tool_call, config, state, cast("Any", runtime)
)
if fallback:
counts = counters or _default_counters(ApprovalMode.AUTO)
reason = f"reason: {fallback_reason}; " if fallback_reason else ""
action["description"] = (
"Auto human fallback "
f"({reason}consecutive denials: {counts['consecutive_denials']}, "
f"classifier unavailable: {counts['consecutive_unavailable']}, "
f"total denials: {counts['total_denials']}).\n\n"
"Auto human fallback: this action needs your review.\n\n"
f"{action.get('description', '')}"
)
return action, review
@@ -3023,8 +3016,6 @@ class AutoModeHITLMiddleware(HumanInTheLoopMiddleware[AutoModeState, Any, Any]):
state,
runtime,
fallback=fallback,
counters=counters,
fallback_reason=fallback_reason,
)
action_requests.append(action)
review_configs.append(review)
@@ -3084,7 +3075,7 @@ class AutoModeHITLMiddleware(HumanInTheLoopMiddleware[AutoModeState, Any, Any]):
manual_reviews: list[ReviewConfig] = []
for call in manual_calls:
action, review = self._action_and_config(
call, state, runtime, fallback=False, counters=counters
call, state, runtime, fallback=False
)
manual_actions.append(action)
manual_reviews.append(review)
@@ -172,7 +172,7 @@ class ApprovalMenu(Container):
self._tool_names = [r.get("name", "unknown") for r in self._action_requests]
self._is_auto_fallback = any(
isinstance(request.get("description"), str)
and request["description"].startswith("Auto human fallback ")
and request["description"].startswith("Auto human fallback")
for request in self._action_requests
)
# Only offer the Auto option when it can actually be enabled. A live
+54
View File
@@ -17863,6 +17863,30 @@ class TestIsUserTyping:
class TestRequestApprovalBranching:
"""_request_approval should show a placeholder when the user is typing."""
async def test_auto_fallback_skips_shell_allow_list(self) -> None:
from deepagents_code.config import settings
app = DeepAgentsApp(agent=MagicMock())
app._last_typed_at = None
action_requests = [
{
"name": "execute",
"args": {"command": "echo hello"},
"description": "Auto human fallback: this action needs your review.",
}
]
with (
patch.object(settings, "shell_allow_list", ["echo"]),
patch.object(app, "_mount_approval_widget", new=AsyncMock()) as mount,
patch.object(app, "_reveal_pending_tool_calls"),
patch.object(app, "_pause_loading_spinner_for_approval"),
):
result = await app._request_approval(action_requests, None)
assert not result.done()
mount.assert_awaited_once()
async def test_placeholder_mounted_when_typing(self) -> None:
"""If the user is typing, a Static placeholder is mounted instead of menu."""
app = DeepAgentsApp(agent=MagicMock())
@@ -30527,6 +30551,36 @@ class TestLiveApprovalModeWrites:
warn.assert_called_once()
assert "Could not open the Auto confirmation" in str(warn.call_args.args[0])
async def test_server_auto_fallback_hides_diagnostics(
self, caplog: pytest.LogCaptureFixture
) -> None:
app = DeepAgentsApp()
event = {
"event": "fallback",
"reason": "consecutive_policy_denials",
"consecutive_denials": 4,
"consecutive_unavailable": 0,
"total_denials": 4,
}
caplog.set_level(logging.DEBUG, logger="deepagents_code.app")
with patch.object(app, "_mount_message", new=AsyncMock()) as mount:
await app._on_auto_mode_event(event)
await_args = mount.await_args
assert await_args is not None
mounted = await_args.args[0]
assert isinstance(mounted, AppMessage)
assert mounted._content == (
"Auto couldn't confidently approve this action, so it needs your review. "
"Auto will continue afterward."
)
assert (
"Auto human fallback reason=consecutive_policy_denials "
"consecutive_denials=4 consecutive_unavailable=0 total_denials=4"
in caplog.messages
)
async def test_server_manual_fallback_updates_tui_mode_and_warns(self) -> None:
from deepagents_code.approval_mode import ApprovalMode
+7 -1
View File
@@ -1917,7 +1917,13 @@ async def test_unavailable_auto_control_state_surfaces_manual_fallback(
hitl_request = review.call_args.args[0]
description = hitl_request["action_requests"][0]["description"]
assert description.startswith("Auto human fallback ")
assert description.startswith(
"Auto human fallback: this action needs your review.\n\n"
)
assert "consecutive denials" not in description
assert "classifier unavailable" not in description
assert "total denials" not in description
assert "Auto control state was unavailable" not in description
assert events == [
{
"type": "auto_mode",
@@ -359,7 +359,7 @@ class TestOptionOrdering:
{
"name": "delete",
"args": {"file_path": "old.py"},
"description": "Auto human fallback (consecutive denials: 3).",
"description": "Auto human fallback: this action needs your review.",
}
)
menu.set_future(future)
@@ -539,7 +539,7 @@ class TestAutoOptionEligibility:
{
"name": "delete",
"args": {"file_path": "old.py"},
"description": "Auto human fallback (consecutive denials: 3).",
"description": "Auto human fallback: this action needs your review.",
},
auto_mode_eligible=False,
)