"""Compatibility helper for explicit agent stop producers.""" from __future__ import annotations import inspect from typing import Any def _accepts_keyword(callable_obj: Any, name: str) -> bool: """Return whether a callable explicitly supports a keyword argument.""" try: parameters = inspect.signature(callable_obj).parameters.values() except (TypeError, ValueError): return False return any( parameter.kind is inspect.Parameter.VAR_KEYWORD or ( parameter.name == name and parameter.kind is not inspect.Parameter.POSITIONAL_ONLY ) for parameter in parameters ) def request_hard_interrupt( agent: Any, message: str | None = None, *, tool_reason: str | None = None, ) -> bool: """Request an explicit stop, falling back to the legacy interrupt ABI. New agents expose ``hard_interrupt(message=None)``. Third-party agents and old test doubles may only expose ``interrupt(message=None)``; keep those usable without sending newer keyword arguments they do not know. ``message`` is diagnostic/control-plane text. ``tool_reason`` is a trusted, fixed category that may be exposed in model-visible tool cancellation output. It is only forwarded when the modern callable explicitly supports that channel. Returns ``False`` only when neither callable is available. """ # Avoid treating a dynamic ``__getattr__`` proxy (notably an unspecced # ``MagicMock`` or a third-party RPC facade) as if it genuinely implements # the new ABI. Static lookup proves the attribute exists on the instance or # its type before normal descriptor binding retrieves the callable. try: inspect.getattr_static(agent, "hard_interrupt") except AttributeError: interrupt = None else: interrupt = getattr(agent, "hard_interrupt", None) if not callable(interrupt): interrupt = getattr(agent, "interrupt", None) if not callable(interrupt): return False kwargs = {} if tool_reason is not None and _accepts_keyword(interrupt, "tool_reason"): kwargs["tool_reason"] = tool_reason if message is None: interrupt(**kwargs) else: interrupt(message, **kwargs) return True