- deadline: _result()/_abandon() replace 7 BoundedResult constructions and 2 cancel+callback sites; timers handled as a list; dead raise_if_timed_out removed. - file_safety: retired classify_cross_profile_target (0 refs; get_cross_profile_warning stub kept for external callers), _home_and_resolved/_mirror_warning shared by the sandbox/container mirror guards; _find_sandbox_mirror_segments inlined. - estop: _hermes_home/_canonical_root now the file_safety helpers; _reset_log_state_for_tests inlined into its only test. - subagent_lifecycle: _validate_request driven by _UNSUPPORTED_REQUEST_FIELDS table. - turn_liveness: dead start()/_abort_message removed, _emit_warning shared. - process_bootstrap: _enable_happy_eyeballs reused by the client variant. - Comment/docstring compaction across the remaining leaf modules.
96 lines
3.4 KiB
Python
96 lines
3.4 KiB
Python
"""Turn-end guard for kanban workers.
|
|
|
|
Kanban workers must end with ``kanban_complete`` or ``kanban_block``. Some
|
|
models narrate the next step and stop with no tool calls; Hermes treats that
|
|
as a clean exit → ``rc=0`` → dispatcher ``protocol_violation``. Policy-only:
|
|
return a bounded synthetic nudge so the loop continues instead of exiting.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any, Iterable, Optional
|
|
|
|
|
|
_TERMINAL_KANBAN_TOOLS = frozenset({"kanban_complete", "kanban_block"})
|
|
|
|
_DEFAULT_MAX_ATTEMPTS = 2
|
|
|
|
|
|
def kanban_stop_nudge_enabled() -> bool:
|
|
"""On when ``HERMES_KANBAN_TASK`` is set, unless ``HERMES_KANBAN_STOP_NUDGE`` disables it."""
|
|
env = os.environ.get("HERMES_KANBAN_STOP_NUDGE")
|
|
if env is not None and env.strip().lower() in {"0", "false", "no", "off"}:
|
|
return False
|
|
return bool((os.environ.get("HERMES_KANBAN_TASK") or "").strip())
|
|
|
|
|
|
def _tool_call_name(tc: Any) -> str:
|
|
if isinstance(tc, dict):
|
|
fn = tc.get("function")
|
|
if isinstance(fn, dict):
|
|
return str(fn.get("name") or "")
|
|
return str(tc.get("name") or "")
|
|
fn = getattr(tc, "function", None)
|
|
if fn is not None:
|
|
return str(getattr(fn, "name", "") or "")
|
|
return str(getattr(tc, "name", "") or "")
|
|
|
|
|
|
def session_called_kanban_terminal(messages: Iterable[dict] | None) -> bool:
|
|
"""True if this conversation already invoked a terminal kanban tool."""
|
|
if not messages:
|
|
return False
|
|
for msg in messages:
|
|
if not isinstance(msg, dict):
|
|
continue
|
|
role = msg.get("role")
|
|
if role == "assistant":
|
|
if any(_tool_call_name(tc) in _TERMINAL_KANBAN_TOOLS for tc in msg.get("tool_calls") or []):
|
|
return True
|
|
elif role == "tool" and str(msg.get("name") or "") in _TERMINAL_KANBAN_TOOLS:
|
|
return True
|
|
return False
|
|
|
|
|
|
def build_kanban_stop_nudge(
|
|
*,
|
|
messages: Iterable[dict] | None = None,
|
|
attempts: int = 0,
|
|
max_attempts: int = _DEFAULT_MAX_ATTEMPTS,
|
|
task_id: Optional[str] = None,
|
|
) -> Optional[str]:
|
|
"""Synthetic follow-up when a kanban worker exits without a terminal tool.
|
|
|
|
``None`` when the guard should not fire (not a kanban worker, already
|
|
completed/blocked, or nudge budget exhausted).
|
|
"""
|
|
if (
|
|
not kanban_stop_nudge_enabled()
|
|
or attempts >= max_attempts
|
|
or session_called_kanban_terminal(messages)
|
|
):
|
|
return None
|
|
|
|
tid = (task_id or os.environ.get("HERMES_KANBAN_TASK") or "").strip() or "this task"
|
|
return (
|
|
"[System: You are a Hermes kanban worker. A plain-text reply is NOT a "
|
|
"terminal state for the board.\n\n"
|
|
f"Task `{tid}` is still `running`. Ending now without a board tool "
|
|
"causes a protocol violation (clean exit with no "
|
|
"`kanban_complete` / `kanban_block`).\n\n"
|
|
"Do this immediately in your next response — do not narrate intent:\n"
|
|
"1. Finish any remaining deliverable (write the required file(s) now).\n"
|
|
"2. Call `kanban_complete(summary=..., artifacts=[...])` if the work "
|
|
"is done, OR `kanban_block(reason=...)` if you are blocked.\n\n"
|
|
"Never end a turn with only a promise of future action. Repeated "
|
|
"protocol violations will block this task and require manual intervention.]"
|
|
)
|
|
|
|
|
|
__all__ = [
|
|
"build_kanban_stop_nudge",
|
|
"kanban_stop_nudge_enabled",
|
|
"session_called_kanban_terminal",
|
|
]
|