refactor(tools): compact kanban/delegation/interrupt/desktop tool modules; drop dead helpers
This commit is contained in:
+24
-49
@@ -1,15 +1,9 @@
|
||||
"""Per-thread interrupt signaling for all tools.
|
||||
|
||||
Thread-scoped interrupt tracking so that interrupting one agent session does
|
||||
not kill tools running in other sessions — critical in the gateway where
|
||||
multiple agents run concurrently in one process. The agent stores its
|
||||
execution thread ID at the start of run_conversation() and passes it to
|
||||
Thread-scoped so interrupting one agent session does not kill tools running in
|
||||
other sessions (the gateway runs many agents in one process). The agent stores
|
||||
its execution thread id at the start of run_conversation() and passes it to
|
||||
set_interrupt(); tools call is_interrupted(), which checks the CURRENT thread.
|
||||
|
||||
Usage in tools:
|
||||
from tools.interrupt import is_interrupted
|
||||
if is_interrupted():
|
||||
return {"output": "[interrupted]", "returncode": 130}
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -19,13 +13,12 @@ import threading
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Opt-in debug tracing — pairs with HERMES_DEBUG_INTERRUPT in
|
||||
# tools/environments/base.py. Logs caller thread, target thread, and current
|
||||
# state per set/check for "interrupt signaled but tool never saw it" reports.
|
||||
# tools/environments/base.py; logs caller/target thread and state per set/check.
|
||||
_DEBUG_INTERRUPT = bool(os.getenv("HERMES_DEBUG_INTERRUPT"))
|
||||
|
||||
if _DEBUG_INTERRUPT:
|
||||
# AIAgent's quiet_mode path forces the `tools` logger to ERROR on CLI
|
||||
# startup; force ours back to INFO so the trace is visible in agent.log.
|
||||
# AIAgent's quiet_mode forces the `tools` logger to ERROR on CLI startup;
|
||||
# force ours back to INFO so the trace is visible in agent.log.
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# Interrupted thread idents, plus an optional user-safe cause per signal. The
|
||||
@@ -35,15 +28,9 @@ _interrupt_reasons: dict[int, str] = {}
|
||||
_lock = threading.Lock()
|
||||
|
||||
|
||||
def set_interrupt(
|
||||
active: bool,
|
||||
thread_id: int | None = None,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
) -> None:
|
||||
"""Set (``active=True``) or clear the interrupt for *thread_id*, defaulting
|
||||
to the current thread (backward compat for CLI/tests). ``reason`` is an
|
||||
optional user-safe cause."""
|
||||
def set_interrupt(active: bool, thread_id: int | None = None, *, reason: str | None = None) -> None:
|
||||
"""Set or clear the interrupt for *thread_id* (default: current thread, for
|
||||
CLI/tests). ``reason`` is an optional user-safe cause."""
|
||||
tid = thread_id if thread_id is not None else threading.current_thread().ident
|
||||
with _lock:
|
||||
if active:
|
||||
@@ -60,8 +47,7 @@ def set_interrupt(
|
||||
logger.info(
|
||||
"[interrupt-debug] set_interrupt(active=%s, target_tid=%s) "
|
||||
"called_from_tid=%s current_set=%s",
|
||||
active, tid, threading.current_thread().ident, _snapshot,
|
||||
)
|
||||
active, tid, threading.current_thread().ident, _snapshot)
|
||||
|
||||
|
||||
def is_interrupted() -> bool:
|
||||
@@ -70,12 +56,9 @@ def is_interrupted() -> bool:
|
||||
|
||||
|
||||
def is_thread_interrupted(thread_id: int | None) -> bool:
|
||||
"""Check whether *thread_id* has an interrupt bit set (``None`` never is).
|
||||
|
||||
Used when a wait is moved onto a deadline worker (``run_bounded_sync``)
|
||||
so ``/stop`` targeting the original tool-worker tid still kills the
|
||||
subprocess.
|
||||
"""
|
||||
"""Whether *thread_id* has an interrupt bit set (``None`` never is). Used when
|
||||
a wait moves onto a deadline worker (``run_bounded_sync``) so ``/stop``
|
||||
targeting the original tool-worker tid still kills the subprocess."""
|
||||
if thread_id is None:
|
||||
return False
|
||||
with _lock:
|
||||
@@ -83,35 +66,27 @@ def is_thread_interrupted(thread_id: int | None) -> bool:
|
||||
|
||||
|
||||
def get_interrupt_reason() -> str | None:
|
||||
"""Return the user-safe interrupt cause for the current thread, if known."""
|
||||
tid = threading.current_thread().ident
|
||||
"""User-safe interrupt cause for the current thread, if known."""
|
||||
with _lock:
|
||||
return _interrupt_reasons.get(tid)
|
||||
return _interrupt_reasons.get(threading.current_thread().ident)
|
||||
|
||||
|
||||
def clear_current_thread_interrupt() -> None:
|
||||
"""Clear any interrupt bit on the CURRENT thread.
|
||||
|
||||
Gives a user-approved command a clean interrupt slate immediately before
|
||||
it spawns its child process, so a stale bit that landed on this thread
|
||||
during the blocking approval-wait cannot SIGINT the just-approved run
|
||||
(exit 130 + "[Command interrupted]"). Single-thread ordering on this tid
|
||||
keeps the DO-NOT-BREAK invariant intact: a *genuine* interrupt arriving
|
||||
after this call re-sets the bit on the same thread and is still observed by
|
||||
the executor's poll loop. Call this directly, never via the
|
||||
_interrupt_event proxy (its .clear() binds to whatever thread runs it).
|
||||
Gives a user-approved command a clean slate right before it spawns its child,
|
||||
so a stale bit that landed during the blocking approval-wait cannot SIGINT the
|
||||
just-approved run. Single-thread ordering keeps the invariant: a *genuine*
|
||||
interrupt arriving after this call re-sets the bit and is still observed by the
|
||||
executor's poll loop. Call directly, never via the _interrupt_event proxy (its
|
||||
.clear() binds to whatever thread runs it).
|
||||
"""
|
||||
set_interrupt(False) # thread_id=None -> current thread
|
||||
set_interrupt(False)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Backward-compatible _interrupt_event proxy: legacy call sites
|
||||
# (code_execution_tool, process_registry, tests) import it and call
|
||||
# .is_set() / .set() / .clear(); the shim maps those to the per-thread API.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _ThreadAwareEventProxy:
|
||||
"""Drop-in proxy that maps threading.Event methods to per-thread state."""
|
||||
"""Backward-compatible ``_interrupt_event``: legacy call sites call
|
||||
.is_set()/.set()/.clear(); the shim maps those to the per-thread API."""
|
||||
|
||||
def is_set(self) -> bool:
|
||||
return is_interrupted()
|
||||
|
||||
Reference in New Issue
Block a user