Files
hermes-agent/agent/kanban_stop.py
Teknium 8fb678ce5d refactor(agent/image+safety): route vision probes/inline executors on data tables, dedupe path guards (-25% LOC)
- image_routing: capability probes -> _VISION_PROBES ordered table; magic-byte
  sniffing -> _MAGIC signature table; bool tokens -> dict; shared code-span /
  file-existence helpers for extract_image_refs. Fixture-corpus diff base vs
  new identical.
- inline_tool_executors: 11 hand-written executor bodies -> _tool/_callback_tool
  arg-spec factories (kwargs mapping unchanged; corpus of calls identical).
- file_safety: unify _is_under/_under_any, _resolve_target/_resolve_each,
  _mirror_info; deny-list tables compacted; drop retired
  get_cross_profile_warning stub (+ its tests). Every rule preserved (corpus).
- image_gen_provider: drop _images_cache_dir (test-only wrapper), compact.
- estop/delegation_context/kanban_stop/image_gen_registry: defensive-layer
  collapse and docstring compaction.

Tool schema dump byte-identical to base.
2026-09-02 18:58:42 -07:00

80 lines
3.3 KiB
Python

"""Turn-end guard for kanban workers, which 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."""
if (os.environ.get("HERMES_KANBAN_STOP_NUDGE") or "").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:
"""Tool name from a dict or object tool call (``function.name`` first, then ``name``)."""
if isinstance(tc, dict):
fn = tc.get("function")
return str((fn.get("name") if isinstance(fn, dict) else tc.get("name")) or "")
fn = getattr(tc, "function", None)
return str((getattr(fn, "name", "") if fn is not None else getattr(tc, "name", "")) or "")
def session_called_kanban_terminal(messages: Iterable[dict] | None) -> bool:
"""True if this conversation already invoked a terminal kanban tool."""
for msg in filter(lambda m: isinstance(m, dict), messages or ()):
role = msg.get("role")
if role == "assistant" and any(
_tool_call_name(tc) in _TERMINAL_KANBAN_TOOLS for tc in msg.get("tool_calls") or []
):
return True
if 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, 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"]