feat(agent): add pre_verify hook and verify-on-stop coding guidance

Add a `pre_verify` user/plugin/shell hook fired once per turn when the agent
edited code and is about to finish, after the existing verify-on-stop guard. A
hook can keep the agent going one more turn (run a check, defer it, tidy the
diff) by returning {"action":"continue","message":...} (the Claude-Code Stop
shape {"decision":"block","reason":...} is accepted too). Hooks receive coding,
attempt, final_response, and sorted changed_paths so they can self-scope and
self-throttle; the path is bounded by agent.max_verify_nudges and preserves
message-role alternation.

Hermes still ships its default coding guidance (agent.verify_guidance, on by
default), but it now rides the evidence-based verify-on-stop missing-evidence
nudge instead of a separate default pre_verify continuation, so it costs no
extra model turn of its own. Guidance reuses the shared utils.is_truthy_value
parser rather than a local copy.
This commit is contained in:
Brooklyn Nicholson 2026-06-30 00:59:29 -05:00
parent 14c4a849b7
commit a10113658b
14 changed files with 458 additions and 1 deletions

View file

@ -136,6 +136,17 @@ VALID_HOOKS: Set[str] = {
"transform_llm_output",
"pre_llm_call",
"post_llm_call",
# Verification-loop gate. Fired once per turn when the agent has edited code
# and is about to verify/finish (after the verify-on-stop guard). A callback
# may keep the agent going — run a check, defer it, tidy the diff — instead
# of stopping by returning:
# {"action": "continue", "message": "<follow-up instruction>"}
# The Claude-Code Stop shape {"decision": "block", "reason": "..."} (block
# the stop == keep going) is accepted too. Anything else lets the turn
# finish. Hermes' shipped guidance lives in the evidence-based
# verification-stop nudge; this hook is for user/plugin policy and is
# bounded by agent.max_verify_nudges.
"pre_verify",
"pre_api_request",
"post_api_request",
"api_request_error",
@ -2029,6 +2040,57 @@ def get_pre_tool_call_block_message(
return None
def get_pre_verify_continue_message(
*,
session_id: str = "",
platform: str = "",
model: str = "",
coding: bool = False,
attempt: int = 0,
final_response: str = "",
changed_paths: Optional[List[str]] = None,
) -> Optional[str]:
"""Check user ``pre_verify`` hooks for a directive to keep the agent going.
Fired once per turn when the agent edited code and is about to verify/finish.
A hook keeps the turn going (run a check, defer it, tidy the diff) by
returning::
{"action": "continue", "message": "<follow-up for the model>"}
The Claude-Code Stop shape ``{"decision": "block", "reason": "..."}`` (block
the stop == keep going) is accepted too. The first directive carrying a
non-empty message wins; any other return lets the turn finish. Mirrors
:func:`get_pre_tool_call_block_message` the call site stays a one-liner.
``coding`` / ``attempt`` let a hook scope itself (``if not coding`` ) and
self-throttle (``if attempt`` ), the same way a ``pre_tool_call`` hook
scopes on ``tool_name``.
"""
hook_results = invoke_hook(
"pre_verify",
session_id=session_id,
platform=platform,
model=model,
coding=coding,
attempt=attempt,
final_response=final_response,
changed_paths=list(changed_paths or []),
)
for result in hook_results:
if not isinstance(result, dict):
continue
action = str(result.get("action") or result.get("decision") or "").strip().lower()
if action not in ("continue", "block"):
continue
message = result.get("message") or result.get("reason")
if isinstance(message, str) and message.strip():
return message.strip()
return None
def _ensure_plugins_discovered(force: bool = False) -> PluginManager:
"""Return the global manager after ensuring plugin discovery has run.