feat: remote agents honor interrupt/stop/play control signals#442
feat: remote agents honor interrupt/stop/play control signals#442amit-gazal-thenvoi wants to merge 8 commits into
Conversation
Wires agent_control into the bridge's AgentRunner: interrupt/stop cancel whatever forward task is currently in flight for a room, and play proactively nudges the room via /next instead of waiting for the next natural event. The bridge holds no Band lifecycle logic, so interrupt and stop are handled identically here — the consume-vs-replay distinction and durable per-room suppression already happen correctly downstream via OneShotInvoker's /next claim check and the platform's stop gate.
The ReconnectedEvent path in ExecutionContext._process_event always ran the full /next sync, unlike the idle-timeout and resync-sentinel paths which already short-circuit on _stopped. The platform gate made this safe (a stopped room's /next is guaranteed empty), but it wasted a call on every reconnect while stopped. Skip locally for consistency with the other _stopped short-circuits.
… sync A stop control signal landing mid-cycle during _synchronize_with_next or _resync_pending_messages left the message in 'processing' for replay, but the enclosing loop kept polling /next, which returns 'processing' messages same as before. The very next iteration re-claimed and fully re-ran the cycle stop just aborted. Both loops now check self._stopped after processing a backlog message and stop polling until play resumes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…y budget A cycle cancelled by our own stop/interrupt control signal never actually ran the handler, but record_attempt() was still charging it as a real attempt. At the default max_message_retries=1, a message stopped once and replayed via /next on resume would immediately land in permanently_failed before ever reaching the adapter. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…te-agents-honor-interrupt-and-stopresume # Conflicts: # src/band/client/streaming/client.py # src/band/platform/link.py # src/band/runtime/execution.py # src/band/runtime/platform_runtime.py # uv.lock
|
|
AlexanderZ-Band
left a comment
There was a problem hiding this comment.
See inline comments — main blockers: the in-flight cancellation gaps in the bridge (task registration ordering, play blocking the receive path), the interrupt/stop lifecycle windows in execution.py, and tests/integration/test_control_signals.py failing to import (thenvoi_rest).
- Extract control-signal routing/dedup into ControlSignalHandler (control.py), slimming AgentRunner; keep cancel/nudge/room-list on the runner. - Bridge: track in-flight forwards per room as a set so interrupt/stop cancels the running forward and queued same-room waiters; match/case dispatch with an unknown-mode warning; run play /next nudges as tracked background tasks so a following stop/interrupt isn't blocked on the receive path; flatten nesting. - execution.py: honor an interrupt/stop that lands in the claim->cycle window (before the cancellable task exists) via _cycle_armed/_pending_interrupt and a shared _abort_cycle unwind; queue the durable ack for retry when an interrupted message's mark_processed fails. - retry_tracker: discard_attempt decrements by one instead of clearing, so interrupting a retry doesn't reset the whole retry budget. - forwarder: document that interrupt/stop is best-effort for AgentCore (the in-flight boto3 thread can't be cancelled). - Tests: shared BlockingHandler fake, wait_until helper, and user-owned room fixture (reuses only marker-titled rooms it created, so room-scope stop/play can't 403); typed client for the mention helper; loop_scope=session on the control integration class; cover the claim->cycle-window signal paths. - Remove planning/manual-test scratch docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve tests/conftest.py import conflict: keep `import asyncio` (used by BlockingHandler) and drop `import os` (main replaced os.environ reads with a pydantic-settings CollectionGateSettings). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@AlexanderZ-Band re: splitting the control-signal code out of |
Summary
agent_control:{agent_id}and handlesinterrupt/stop/playpreemptively, out-of-band from the message queue, so a signal can abort a reasoning cycle already in flight (ExecutionContext/AgentRuntime).interruptaborts the in-flight cycle and consumes the message (nothing sent, no replay).stopdoes the same plus durably suppresses new cycles in that room untilplay, which triggers a rehydration-style/nextcatch-up.band-bridge) honors the same signals for one-shot/bridged agents:interrupt/stopcancel whatever forward call is currently in flight for a room;playproactively nudges the room via/nextinstead of waiting for the next natural event.ReconnectedEventsync path now short-circuits locally on_stopped, consistent with the other stopped-guards, instead of making a/nextcall already known to return empty.Test plan
uv run pytest tests/ --ignore=tests/integration/ --ignore=tests/e2e/— full unit suite greenuv run pytest tests/runtime/ tests/platform/ tests/websocket/ tests/bridge/ tests/test_platform_runtime.py— control-signal suites green (851 passed)uv run ruff check ./uv run ruff format --check ./uv run pyrefly check— cleantests/integration/test_control_signals.py) — require a live platform +BAND_API_KEY/BAND_API_KEY_USER; not run in CI🤖 Generated with Claude Code