mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-26 06:01:49 +00:00
feat(goals): /subgoal — user-added criteria appended to active /goal (#25449)
* feat(goals): /subgoal — user-added criteria appended to active /goal Layers a /subgoal command on top of the existing freeform Ralph judge loop. The user can append extra criteria mid-loop; the judge factors them into its done/continue verdict and the continuation prompt surfaces them to the agent. No new tool, no agent self-judging — the existing judge model just sees a richer prompt. Forms: /subgoal show current subgoals /subgoal <text> append a criterion /subgoal remove <n> drop subgoal n (1-based) /subgoal clear wipe all subgoals How it integrates: - GoalState gains `subgoals: List[str]` (default []), backwards-compat for existing state_meta rows. - judge_goal accepts an optional subgoals kwarg; non-empty switches to JUDGE_USER_PROMPT_WITH_SUBGOALS_TEMPLATE which lists them as numbered criteria and asks 'is the goal AND every additional criterion satisfied?' - next_continuation_prompt picks CONTINUATION_PROMPT_WITH_SUBGOALS_TEMPLATE when non-empty so the agent sees what to target. - /subgoal is allowed mid-run on the gateway since it only touches the state the judge reads at turn boundary — no race with the running turn. - Status line shows '... , N subgoals' when present. Surface: - hermes_cli/goals.py — field, prompt blocks, manager methods, judge weave - hermes_cli/commands.py — /subgoal CommandDef - cli.py — _handle_subgoal_command - gateway/run.py — _handle_subgoal_command + mid-run dispatch - tests/hermes_cli/test_goals.py — 15 new tests (backcompat, mutation, persistence, prompt template selection, judge-prompt content via mock, status-line rendering) 77 goal-related tests passing across goals + cli + gateway + tui. * fix(goals): slash commands don't preempt the goal-continuation hook Two findings from live-testing /subgoal: 1. Slash commands queued while the agent is running landed in _pending_input (same queue as real user messages). The goal hook's 'is a real user message pending?' check returned True and silently skipped — but the slash command consumes its queue slot via process_command() which never re-fires the goal hook, so the loop stalls indefinitely. Now the hook peeks the queue and only defers when a non-slash payload is present. 2. The with-subgoals judge prompt was too soft — opus 4.7 said 'done, implying all requirements met' without verifying. Tightened to demand specific per-criterion evidence (file contents, output line, command result) and explicitly reject phrases like 'implying it was done.' Live verified: /subgoal injected mid-loop now correctly forces the judge to refuse done until the new criterion is met. Agent gets the continuation prompt with subgoals listed, updates the script, judge confirms done with specific evidence cited.
This commit is contained in:
parent
d110ce4493
commit
8f19078c6a
5 changed files with 531 additions and 14 deletions
|
|
@ -6173,6 +6173,12 @@ class GatewayRunner:
|
|||
return await self._handle_goal_command(event)
|
||||
return "Agent is running — use /goal status / pause / clear mid-run, or /stop before setting a new goal."
|
||||
|
||||
# /subgoal is safe mid-run — it only modifies the goal's
|
||||
# subgoals list, which the judge reads at the next turn
|
||||
# boundary. No race with the running turn.
|
||||
if _cmd_def_inner and _cmd_def_inner.name == "subgoal":
|
||||
return await self._handle_subgoal_command(event)
|
||||
|
||||
# Session-level toggles that are safe to run mid-agent —
|
||||
# /yolo can unblock a pending approval prompt, /verbose cycles
|
||||
# the tool-progress display mode for the ongoing stream.
|
||||
|
|
@ -6554,6 +6560,9 @@ class GatewayRunner:
|
|||
if canonical == "goal":
|
||||
return await self._handle_goal_command(event)
|
||||
|
||||
if canonical == "subgoal":
|
||||
return await self._handle_subgoal_command(event)
|
||||
|
||||
if canonical == "voice":
|
||||
return await self._handle_voice_command(event)
|
||||
|
||||
|
|
@ -9524,6 +9533,57 @@ class GatewayRunner:
|
|||
|
||||
return t("gateway.goal.set", budget=state.max_turns, goal=state.goal)
|
||||
|
||||
async def _handle_subgoal_command(self, event: "MessageEvent") -> str:
|
||||
"""Handle /subgoal for gateway platforms (mirror of CLI handler).
|
||||
|
||||
Subgoals are extra criteria appended to the active goal mid-loop.
|
||||
They modify state read at the next turn boundary, so this is safe
|
||||
to invoke while the agent is running.
|
||||
"""
|
||||
args = (event.get_command_args() or "").strip()
|
||||
mgr, _session_entry = self._get_goal_manager_for_event(event)
|
||||
if mgr is None:
|
||||
return t("gateway.goal.unavailable")
|
||||
if not mgr.has_goal():
|
||||
return "No active goal. Set one with /goal <text>."
|
||||
|
||||
# No args → list current subgoals.
|
||||
if not args:
|
||||
return f"{mgr.status_line()}\n{mgr.render_subgoals()}"
|
||||
|
||||
tokens = args.split(None, 1)
|
||||
verb = tokens[0].lower()
|
||||
rest = tokens[1].strip() if len(tokens) > 1 else ""
|
||||
|
||||
if verb == "remove":
|
||||
if not rest:
|
||||
return "Usage: /subgoal remove <n>"
|
||||
try:
|
||||
idx = int(rest.split()[0])
|
||||
except ValueError:
|
||||
return "/subgoal remove: <n> must be an integer (1-based index)."
|
||||
try:
|
||||
removed = mgr.remove_subgoal(idx)
|
||||
except (IndexError, RuntimeError) as exc:
|
||||
return f"/subgoal remove: {exc}"
|
||||
return f"✓ Removed subgoal {idx}: {removed}"
|
||||
|
||||
if verb == "clear":
|
||||
try:
|
||||
prev = mgr.clear_subgoals()
|
||||
except RuntimeError as exc:
|
||||
return f"/subgoal clear: {exc}"
|
||||
if prev:
|
||||
return f"✓ Cleared {prev} subgoal{'s' if prev != 1 else ''}."
|
||||
return "No subgoals to clear."
|
||||
|
||||
try:
|
||||
text = mgr.add_subgoal(args)
|
||||
except (ValueError, RuntimeError) as exc:
|
||||
return f"/subgoal: {exc}"
|
||||
idx = len(mgr.state.subgoals) if mgr.state else 0
|
||||
return f"✓ Added subgoal {idx}: {text}"
|
||||
|
||||
async def _send_goal_status_notice(self, source: Any, message: str) -> None:
|
||||
"""Send a /goal judge status line back to the originating chat/thread."""
|
||||
adapter = self.adapters.get(source.platform)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue