Skip to content

feat: remote agents honor interrupt/stop/play control signals#442

Open
amit-gazal-thenvoi wants to merge 8 commits into
mainfrom
int-829-agentflow-remote-agents-honor-interrupt-and-stopresume
Open

feat: remote agents honor interrupt/stop/play control signals#442
amit-gazal-thenvoi wants to merge 8 commits into
mainfrom
int-829-agentflow-remote-agents-honor-interrupt-and-stopresume

Conversation

@amit-gazal-thenvoi

Copy link
Copy Markdown
Collaborator

Summary

  • SDK joins agent_control:{agent_id} and handles interrupt/stop/play preemptively, out-of-band from the message queue, so a signal can abort a reasoning cycle already in flight (ExecutionContext/AgentRuntime).
  • interrupt aborts the in-flight cycle and consumes the message (nothing sent, no replay). stop does the same plus durably suppresses new cycles in that room until play, which triggers a rehydration-style /next catch-up.
  • Bridge (band-bridge) honors the same signals for one-shot/bridged agents: interrupt/stop cancel whatever forward call is currently in flight for a room; play proactively nudges the room via /next instead of waiting for the next natural event.
  • Fix: the ReconnectedEvent sync path now short-circuits locally on _stopped, consistent with the other stopped-guards, instead of making a /next call already known to return empty.

Test plan

  • uv run pytest tests/ --ignore=tests/integration/ --ignore=tests/e2e/ — full unit suite green
  • uv 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 — clean
  • Live integration tests (tests/integration/test_control_signals.py) — require a live platform + BAND_API_KEY/BAND_API_KEY_USER; not run in CI

🤖 Generated with Claude Code

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.
@linear-code

linear-code Bot commented Jul 16, 2026

Copy link
Copy Markdown

INT-829

amit-gazal-thenvoi and others added 2 commits July 16, 2026 16:11
… 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>
@amit-gazal-thenvoi
amit-gazal-thenvoi requested a review from a team July 16, 2026 14:46
…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
Comment thread band-bridge/bridge_core/bridge.py Outdated
Comment thread band-bridge/bridge_core/bridge.py
Comment thread band-bridge/bridge_core/bridge.py Outdated
Comment thread src/band/runtime/execution.py Outdated
Comment thread src/band/runtime/execution.py
Comment thread src/band/runtime/retry_tracker.py Outdated
Comment thread band-bridge/bridge_core/bridge.py Outdated
Comment thread band-bridge/bridge_core/bridge.py Outdated
Comment thread band-bridge/bridge_core/bridge.py
@AlexanderZ-Band

Copy link
Copy Markdown
Collaborator

AgentRunner does forwarding, rehydration, and control signals in one ~800-line file. The control-signal part is self-contained — consider moving it to its own module as a follow-up.

Comment thread tests/integration/test_control_signals.py Outdated
Comment thread tests/integration/test_control_signals.py Outdated
Comment thread tests/integration/test_control_signals.py Outdated
Comment thread tests/integration/test_control_signals.py Outdated
Comment thread tests/integration/test_control_signals.py Outdated

@AlexanderZ-Band AlexanderZ-Band left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Base automatically changed from dev to main July 20, 2026 13:47
amit-gazal-thenvoi and others added 2 commits July 22, 2026 17:47
- 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>
@amit-gazal-thenvoi

Copy link
Copy Markdown
Collaborator Author

@AlexanderZ-Band re: splitting the control-signal code out of AgentRunner — done as part of this review pass (44fc738): the control-signal concern is now its own module, band-bridge/bridge_core/control.py (ControlSignalHandler), which owns dedup + routing and delegates cancel/nudge/room-list back to AgentRunner — trimming the runner and isolating the control logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants