2026-04-11 14:02:58 -07:00
|
|
|
"""Per-thread interrupt signaling for all tools.
|
2026-02-23 02:11:33 -08:00
|
|
|
|
2026-04-11 14:02:58 -07:00
|
|
|
Provides thread-scoped interrupt tracking so that interrupting one agent
|
|
|
|
|
session does not kill tools running in other sessions. This is critical
|
|
|
|
|
in the gateway where multiple agents run concurrently in the same process.
|
|
|
|
|
|
|
|
|
|
The agent stores its execution thread ID at the start of run_conversation()
|
|
|
|
|
and passes it to set_interrupt()/clear_interrupt(). Tools call
|
|
|
|
|
is_interrupted() which checks the CURRENT thread — no argument needed.
|
2026-02-23 02:11:33 -08:00
|
|
|
|
|
|
|
|
Usage in tools:
|
|
|
|
|
from tools.interrupt import is_interrupted
|
|
|
|
|
if is_interrupted():
|
|
|
|
|
return {"output": "[interrupted]", "returncode": 130}
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import threading
|
|
|
|
|
|
2026-04-11 14:02:58 -07:00
|
|
|
# Set of thread idents that have been interrupted.
|
|
|
|
|
_interrupted_threads: set[int] = set()
|
|
|
|
|
_lock = threading.Lock()
|
|
|
|
|
|
2026-02-23 02:11:33 -08:00
|
|
|
|
2026-04-11 14:02:58 -07:00
|
|
|
def set_interrupt(active: bool, thread_id: int | None = None) -> None:
|
|
|
|
|
"""Set or clear interrupt for a specific thread.
|
2026-02-23 02:11:33 -08:00
|
|
|
|
2026-04-11 14:02:58 -07:00
|
|
|
Args:
|
|
|
|
|
active: True to signal interrupt, False to clear it.
|
|
|
|
|
thread_id: Target thread ident. When None, targets the
|
|
|
|
|
current thread (backward compat for CLI/tests).
|
|
|
|
|
"""
|
|
|
|
|
tid = thread_id if thread_id is not None else threading.current_thread().ident
|
|
|
|
|
with _lock:
|
|
|
|
|
if active:
|
|
|
|
|
_interrupted_threads.add(tid)
|
|
|
|
|
else:
|
|
|
|
|
_interrupted_threads.discard(tid)
|
2026-02-23 02:11:33 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_interrupted() -> bool:
|
2026-04-11 14:02:58 -07:00
|
|
|
"""Check if an interrupt has been requested for the current thread.
|
|
|
|
|
|
|
|
|
|
Safe to call from any thread — each thread only sees its own
|
|
|
|
|
interrupt state.
|
|
|
|
|
"""
|
|
|
|
|
tid = threading.current_thread().ident
|
|
|
|
|
with _lock:
|
|
|
|
|
return tid in _interrupted_threads
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Backward-compatible _interrupt_event proxy
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Some legacy call sites (code_execution_tool, process_registry, tests)
|
|
|
|
|
# import _interrupt_event directly and call .is_set() / .set() / .clear().
|
|
|
|
|
# This shim maps those calls to the per-thread functions above so existing
|
|
|
|
|
# code keeps working while the underlying mechanism is thread-scoped.
|
|
|
|
|
|
|
|
|
|
class _ThreadAwareEventProxy:
|
|
|
|
|
"""Drop-in proxy that maps threading.Event methods to per-thread state."""
|
|
|
|
|
|
|
|
|
|
def is_set(self) -> bool:
|
|
|
|
|
return is_interrupted()
|
|
|
|
|
|
|
|
|
|
def set(self) -> None: # noqa: A003
|
|
|
|
|
set_interrupt(True)
|
|
|
|
|
|
|
|
|
|
def clear(self) -> None:
|
|
|
|
|
set_interrupt(False)
|
|
|
|
|
|
|
|
|
|
def wait(self, timeout: float | None = None) -> bool:
|
|
|
|
|
"""Not truly supported — returns current state immediately."""
|
|
|
|
|
return self.is_set()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_interrupt_event = _ThreadAwareEventProxy()
|