fix(goal): /goal <text> kicks the loop with a short pointer when the user's last message already carries the goal

`/goal <text>` queues the goal text as the next user turn to start the loop.
When that text is what the user just pasted (a handoff note, a plan the agent
already has), the kickoff re-sends it verbatim: in the 1,393-agent run
`/goal <2,000-char handoff>` came 16 minutes after the same note was pasted
as a message, and the agent spent 11 API calls / 6 min deciding it was a
replay, with the note duplicated in context from then on.

_goal_kick_prompt() compares the normalized goal against the LAST user
message (string or block content); when contained, the kick is a one-line
'[Goal set] Continue with the goal you were just given; there is no need to
re-read it.' Otherwise the goal text is sent as before. Both /goal <text>
and /goal draft use it.

Tests (2): a just-pasted goal (plain and block content) kicks with the
pointer; a new goal, an unrelated last message, and a goal pasted in an
OLDER turn kick verbatim.
This commit is contained in:
Teknium
2026-09-05 02:03:50 -07:00
parent f58fcc8118
commit 7e2dbcfb33
2 changed files with 60 additions and 2 deletions
+23 -2
View File
@@ -2223,6 +2223,27 @@ class CLICommandsMixin:
else:
self._goal_set(mgr, arg)
# `/goal <text>` kicks the loop by queueing the goal text as the next user turn. When that text
# is what the user JUST said (a pasted handoff note, a plan the agent already has), re-sending
# it makes the agent spend a turn deciding it is a replay (11 API calls, 6 min, in one run) and
# duplicates ~2k tokens of context. A short pointer starts the loop just as well.
_GOAL_ALREADY_SEEN_KICK = "[Goal set] Continue with the goal you were just given; there is no need to re-read it."
def _goal_kick_prompt(self, goal: str) -> str:
"""The goal text, or a short pointer when the last user message already carries it."""
last_user = ""
for msg in reversed(getattr(self, "conversation_history", None) or []):
if msg.get("role") == "user":
content = msg.get("content")
if isinstance(content, list):
content = " ".join(str(b.get("text", "")) for b in content if isinstance(b, dict))
last_user = str(content or "")
break
goal_norm = " ".join(goal.split())
if goal_norm and goal_norm in " ".join(last_user.split()):
return self._GOAL_ALREADY_SEEN_KICK
return goal
def _kick_goal(self, prompt: str) -> bool:
"""Queue ``prompt`` as the next turn so the loop starts without a separate message."""
try:
@@ -2310,7 +2331,7 @@ class CLICommandsMixin:
_cp(_dim_line(f"After each turn, a judge model checks if the goal is done{against}. "
"Hermes keeps working until it is, you pause/clear it, or the budget is "
"exhausted. Use /goal status, /goal show, /goal pause, /goal resume, /goal clear."))
self._kick_goal(state.goal)
self._kick_goal(self._goal_kick_prompt(state.goal))
def _print_goal_set(self, state, contract_label: str) -> None:
_cp(f" ⊙ Goal set ({state.max_turns}-turn budget): {state.goal}")
@@ -2343,7 +2364,7 @@ class CLICommandsMixin:
else:
_cp(_dim_line("Couldn't draft a contract (aux model unavailable) — running as a "
"free-form goal. The per-turn judge still applies."))
self._kick_goal(state.goal)
self._kick_goal(self._goal_kick_prompt(state.goal))
def _handle_loop_command(self, cmd: str) -> None:
"""Dispatch /loop — recurring in-session wakeups: ``/loop [interval] <prompt> [--times N]
+37
View File
@@ -0,0 +1,37 @@
"""`/goal <text>` kicks the loop with a short pointer when the user's last message already carries the goal.
In one run `/goal <2,000-char handoff note>` was issued 16 minutes after the same note had been pasted as
a user message; the kickoff re-sent it and the agent spent 11 API calls / 6 min deciding it was a replay.
"""
import queue
from hermes_cli.cli_commands_mixin import CLICommandsMixin
def _cli(history):
cli = CLICommandsMixin.__new__(CLICommandsMixin)
cli.conversation_history = history
cli._pending_input = queue.Queue()
return cli
HANDOFF = "HANDOFF: resume round 3 integration.\n - merge r3-16 first\n - then run the full suite"
def test_goal_that_the_user_just_pasted_kicks_with_a_pointer_not_the_text():
cli = _cli([{"role": "user", "content": "Here is the plan.\n\n" + HANDOFF + "\n\nGo."},
{"role": "assistant", "content": "ok"}])
assert cli._goal_kick_prompt(HANDOFF) == CLICommandsMixin._GOAL_ALREADY_SEEN_KICK
# block-style content is handled too
cli = _cli([{"role": "user", "content": [{"type": "text", "text": HANDOFF}]}])
assert cli._goal_kick_prompt(HANDOFF) == CLICommandsMixin._GOAL_ALREADY_SEEN_KICK
def test_a_new_goal_or_a_goal_from_an_older_turn_is_kicked_verbatim():
assert _cli([])._goal_kick_prompt("Ship the release") == "Ship the release"
cli = _cli([{"role": "user", "content": "Something unrelated"}])
assert cli._goal_kick_prompt(HANDOFF) == HANDOFF
# only the LAST user message counts: the agent has moved on since an older paste
cli = _cli([{"role": "user", "content": HANDOFF}, {"role": "assistant", "content": "done"},
{"role": "user", "content": "now something else"}])
assert cli._goal_kick_prompt(HANDOFF) == HANDOFF