From 7e2dbcfb33416fc38c957a422343bea600bd1f6a Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sat, 5 Sep 2026 02:03:50 -0700 Subject: [PATCH] fix(goal): /goal kicks the loop with a short pointer when the user's last message already carries the goal `/goal ` 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 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. --- hermes_cli/cli_commands_mixin.py | 25 +++++++++++++++-- tests/cli/test_cli_goal_kick_prompt.py | 37 ++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 tests/cli/test_cli_goal_kick_prompt.py diff --git a/hermes_cli/cli_commands_mixin.py b/hermes_cli/cli_commands_mixin.py index 84fe41b0a0..7f58ad33cd 100644 --- a/hermes_cli/cli_commands_mixin.py +++ b/hermes_cli/cli_commands_mixin.py @@ -2223,6 +2223,27 @@ class CLICommandsMixin: else: self._goal_set(mgr, arg) + # `/goal ` 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] [--times N] diff --git a/tests/cli/test_cli_goal_kick_prompt.py b/tests/cli/test_cli_goal_kick_prompt.py new file mode 100644 index 0000000000..ae57abc900 --- /dev/null +++ b/tests/cli/test_cli_goal_kick_prompt.py @@ -0,0 +1,37 @@ +"""`/goal ` 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