mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 01:35:28 -04:00
fix(code): clear stale live approval mode keys (#4242)
When a user turns live approval mode on or off, the app keeps a session key for the live approval-mode state so it can update or clean up that state later. If the current agent cannot provide that live state writer, the app should treat the write as failed and forget any old key instead of holding on to stale state. The user story is: a user toggles live approval mode while a session is active, but the active agent is one that can run without writing to the shared live approval-mode store. That can happen with local or in-process agents, older/custom agent wrappers, test doubles, or partially initialized agents that do not expose the remote store writer used for live approval-mode updates. In that situation, keeping an old key would make the app think it still has a valid live approval-mode record even though the current agent cannot update it. This change makes the no-writer case fail safely. The app clears the stale key, reports the write as unsuccessful, and falls back to the existing approval behavior instead of carrying an outdated live approval-mode key into later tool approval checks.
This commit is contained in:
@@ -5576,6 +5576,14 @@ class DeepAgentsApp(App):
|
||||
self._session_state.approval_mode_key = None
|
||||
logger.warning("Failed to write live approval-mode state", exc_info=True)
|
||||
return False
|
||||
if live_key is None:
|
||||
# No store writer on the agent (a local/in-process agent rather
|
||||
# than a RemoteAgent). This is an expected configuration, not a
|
||||
# fault, so — unlike the except branch above — we clear the stale
|
||||
# key and fail closed without logging, to avoid noise on every
|
||||
# toggle. The run-context path persists the mode for local agents.
|
||||
self._session_state.approval_mode_key = None
|
||||
return False
|
||||
self._session_state.approval_mode_key = live_key
|
||||
return True
|
||||
|
||||
|
||||
@@ -13330,6 +13330,18 @@ class TestLiveApprovalModeWrites:
|
||||
assert not await app._write_live_approval_mode()
|
||||
assert app._session_state.approval_mode_key is None
|
||||
|
||||
async def test_write_live_approval_mode_fails_without_writer(self) -> None:
|
||||
app = DeepAgentsApp()
|
||||
app._agent = object()
|
||||
app._session_state = TextualSessionState(
|
||||
thread_id="thread-1",
|
||||
auto_approve=False,
|
||||
)
|
||||
app._session_state.approval_mode_key = "stale"
|
||||
|
||||
assert not await app._write_live_approval_mode()
|
||||
assert app._session_state.approval_mode_key is None
|
||||
|
||||
async def test_toggle_off_failed_write_cancels_running_agent(self) -> None:
|
||||
app = DeepAgentsApp(auto_approve=True)
|
||||
async with app.run_test() as pilot:
|
||||
@@ -13358,6 +13370,35 @@ class TestLiveApprovalModeWrites:
|
||||
notify.assert_called_once()
|
||||
assert notify.call_args.kwargs["severity"] == "warning"
|
||||
|
||||
async def test_toggle_off_no_writer_cancels_running_agent(self) -> None:
|
||||
# Unlike the test above, this drives a *real* writer-less agent
|
||||
# (no `aput_store_item`) so the False return originates from the
|
||||
# `live_key is None` branch rather than a mock, proving that the
|
||||
# no-writer condition actually reaches the mid-run cancel path.
|
||||
app = DeepAgentsApp(auto_approve=True)
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app._agent = object()
|
||||
app._session_state = TextualSessionState(
|
||||
thread_id="thread-1",
|
||||
auto_approve=True,
|
||||
)
|
||||
app._session_state.approval_mode_key = "stale"
|
||||
app._agent_running = True
|
||||
with (
|
||||
patch.object(app, "_force_interrupt_active_work") as force,
|
||||
patch.object(app, "notify") as notify,
|
||||
):
|
||||
await app.action_toggle_auto_approve()
|
||||
|
||||
assert app._auto_approve is False
|
||||
assert app._session_state.auto_approve is False
|
||||
assert app._session_state.approval_mode_key is None
|
||||
force.assert_called_once()
|
||||
notify.assert_called_once()
|
||||
assert notify.call_args.kwargs["severity"] == "warning"
|
||||
assert "cancelled for safety" in notify.call_args.args[0]
|
||||
|
||||
async def test_toggle_off_failed_write_does_not_cancel_when_idle(self) -> None:
|
||||
app = DeepAgentsApp(auto_approve=True)
|
||||
async with app.run_test() as pilot:
|
||||
|
||||
Reference in New Issue
Block a user