fix: ACP set_model no longer runs queued prompts inside the RPC (review follow-up)
set_session_model's `finally` awaited _drain_queued_prompts, so a prompt queued during a failed (or slow) switch_model ran a whole agent turn inside the session/set_model request; the client received the RequestError only after that turn ended, and the drained turn streamed with no open prompt request. Schedule the drain via _schedule_soon so it runs right after the response is queued, on both the success and the error path.
This commit is contained in:
@@ -1041,7 +1041,10 @@ class HermesACPAgent(SlashCommandsMixin, acp.Agent):
|
||||
finally:
|
||||
with state.runtime_lock:
|
||||
state.command_op = False
|
||||
await self._drain_queued_prompts(state, session_id, self._conn)
|
||||
# Drain AFTER this response is queued, never inside it: a prompt that arrived
|
||||
# mid-switch would otherwise run a whole turn before the client sees the
|
||||
# (possibly failed) switch result.
|
||||
self._schedule_soon(lambda: self._drain_queued_prompts(state, session_id, self._conn))
|
||||
logger.info(
|
||||
"Session %s: model switched to %s via provider %s", session_id, resolved_model, requested_provider
|
||||
)
|
||||
|
||||
@@ -179,3 +179,42 @@ def test_acp_set_session_model_rejection_is_invalid_params_and_leaves_session_un
|
||||
asyncio.run(agent.set_session_model("other", "s1"))
|
||||
assert not isinstance(rebuild_exc.value, RequestError)
|
||||
assert state.model == "claude-sonnet-5" and state.agent is old_agent
|
||||
|
||||
|
||||
def test_acp_set_session_model_does_not_run_queued_prompts_inside_the_rpc(monkeypatch):
|
||||
"""A prompt that arrives while ``switch_model`` is off-loop is queued behind ``command_op``;
|
||||
it must run AFTER the set_model response (error or success) is queued, never inside the RPC —
|
||||
otherwise a failed switch is reported only after a whole agent turn."""
|
||||
import asyncio
|
||||
|
||||
from acp.exceptions import RequestError
|
||||
|
||||
monkeypatch.setattr("hermes_cli.model_switch.switch_model",
|
||||
lambda **_kw: ModelSwitchResult(success=False, error_message="`nope` is not a model"))
|
||||
agent, _made = _acp_agent()
|
||||
state = _state()
|
||||
state.queued_prompts = ["hello, queued mid-switch"]
|
||||
agent.session_manager.get_session = lambda sid: state
|
||||
|
||||
class _Conn:
|
||||
async def session_update(self, *_a, **_k):
|
||||
pass
|
||||
|
||||
agent._conn = _Conn()
|
||||
ran: list = []
|
||||
|
||||
async def _prompt(*, prompt, session_id):
|
||||
ran.append(prompt[0].text)
|
||||
|
||||
agent.prompt = _prompt
|
||||
|
||||
async def _run():
|
||||
with pytest.raises(RequestError):
|
||||
await agent.set_session_model("nope", "s1")
|
||||
assert ran == [], "the queued prompt ran inside the set_model RPC"
|
||||
await asyncio.sleep(0) # let the scheduled drain run once the response is out
|
||||
await asyncio.sleep(0)
|
||||
return list(ran)
|
||||
|
||||
assert asyncio.run(_run()) == ["hello, queued mid-switch"]
|
||||
assert state.command_op is False
|
||||
|
||||
Reference in New Issue
Block a user