mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 13:26:40 +00:00
Independent review of the hold_stream feature found four paths that still let the untransformed reply or commentary reach the user: - History loads showed held commentary. The tool-call assistant row of a held turn is now typed display_kind="hidden" before its first flush (withhold_held_commentary), so TUI/Desktop display_projection, the dashboard and classic CLI resume skip it. display_kind never reaches the wire: the model's context and cached prefix are unchanged (test replays the resumed session and compares the request bytes). The TUI projection still labels the hidden row's tool cards from its calls. Codex held turns type their earlier agentMessages hidden the same way. - A plugin reload mid-turn could empty the hook registry before the final transform. The held turn now snapshots its transform_llm_output callbacks together with the hold decision under the discovery lock (held_stream_transforms) and runs that snapshot at the final. If no callback rewrote and one raised or timed out, the user gets a fixed "reply withheld" notice instead of the original (fail closed). - Classic CLI replay-gap recovery painted and spoke pre_transform_response. A held turn's result (generic and codex) no longer carries the original at all (public_pre_transform), so receipts, recovery and post-stream suffix painting only ever see the rewrite. - Codex error-after-text returned the raw agentMessage. The codex reply is now transformed whenever it has text, also when the turn errored (and, when held, when interrupted). Same class, also fixed: the end-of-stream scrubber flush recorded a held tail as streamed text (an interrupted turn could return it as an untransformed partial final), and quiet-mode CLI printed held commentary via _vprint.
54 lines
2.4 KiB
Python
54 lines
2.4 KiB
Python
"""The codex app-server runtime runs ``transform_llm_output`` like the generic loop.
|
|
|
|
That runtime returns before ``finalize_turn``, so a rewriting plugin (redaction) must be applied
|
|
to the codex reply before its rows are persisted: the user (who, with ``hold_stream``, saw nothing
|
|
stream) receives the rewrite, and the stored assistant row is the rewrite too.
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock
|
|
|
|
from agent.codex_runtime import run_codex_app_server_turn
|
|
from tests.fakes.redactor_plugin import RAW, REWRITE, redactor_home
|
|
|
|
|
|
def _codex_agent(final_text: str):
|
|
agent = MagicMock()
|
|
agent._codex_session = MagicMock() # a seeded session: no subprocess spawn
|
|
agent._codex_session.run_turn.return_value = SimpleNamespace(
|
|
interrupted=False, error=None, thread_id="thread-1", turn_id="turn-1", tool_iterations=0,
|
|
projected_messages=[{"role": "assistant", "content": final_text}], final_text=final_text,
|
|
should_retire=False)
|
|
agent._codex_session_prompt = None
|
|
agent.tool_progress_callback = None
|
|
agent._iters_since_skill = agent._skill_nudge_interval = 0
|
|
agent.valid_tool_names = set()
|
|
agent._session_db = None
|
|
agent._llm_output_transform = None
|
|
agent._hold_stream, agent._held_transforms = False, () # turn-start state; the hold has its own tests
|
|
agent.session_id = "sess-codex"
|
|
agent.platform = "cli"
|
|
return agent
|
|
|
|
|
|
def test_codex_reply_is_rewritten_before_it_is_returned_or_stored():
|
|
from hermes_cli import plugins
|
|
|
|
redactor_home(Path(os.environ["HERMES_HOME"]), "http://127.0.0.1:9/v1", hold=True)
|
|
plugins._reset_plugin_managers_for_tests()
|
|
try:
|
|
agent = _codex_agent(RAW)
|
|
result = run_codex_app_server_turn(
|
|
agent, user_message="config?", original_user_message="config?",
|
|
messages=[{"role": "user", "content": "config?"}], effective_task_id="task-1", turn_id="t1")
|
|
finally:
|
|
plugins._reset_plugin_managers_for_tests()
|
|
|
|
assert result["final_response"] == REWRITE
|
|
assert result["response_transformed"] is True and result["pre_transform_response"] == RAW
|
|
assert [m["content"] for m in result["messages"] if m["role"] == "assistant"] == [REWRITE]
|
|
# External memory keeps the reply the user got, not the text the plugin removed.
|
|
assert agent._sync_external_memory_for_turn.call_args.kwargs["final_response"] == REWRITE
|