mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-16 14:32:34 +00:00
The desktop app's chat panel reuses tui_gateway as its backend, so every chat session was stamped platform="tui". That made the agent read terminal-specific platform guidance while running in the graphical desktop chat surface. Resolve the misclassification at its source: tui_gateway now picks platform="desktop" when HERMES_DESKTOP=1 and HERMES_DESKTOP_TERMINAL is unset, and keeps platform="tui" for the embedded terminal pane and standalone TUI. Add a PLATFORM_HINTS["desktop"] entry describing the actual chat surface (full GFM markdown, MEDIA: intercept, inline images). Move the embedded-pane clarifier to the platform-hint resolution site so it appends only to the tui hint under HERMES_DESKTOP_TERMINAL=1. Delete the now-dead desktop-hint block from build_environment_hints() that competed with the platform hint. Standalone TUI sessions produce byte-identical prompts as before; the new desktop hint and clarifier are assembled once per session in the stable tier, so prompt caching is preserved.
246 lines
11 KiB
Python
246 lines
11 KiB
Python
"""System-prompt assembly for the desktop chat surface.
|
|
|
|
Pins the second half of the fix: the new ``PLATFORM_HINTS["desktop"]``
|
|
entry, the deletion of the standalone desktop-hint block from
|
|
``build_environment_hints()``, and the lookup-site extension that
|
|
appends the embedded-terminal-pane clarifier to the ``tui`` platform hint
|
|
when ``HERMES_DESKTOP_TERMINAL=1``.
|
|
|
|
These tests run against the real prompt builders (no mocks) because
|
|
cache-stability and byte-for-byte text contracts are what we are
|
|
verifying — mocking the resolver would hide exactly the class of bug
|
|
this test covers.
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from agent.prompt_builder import PLATFORM_HINTS, build_environment_hints
|
|
from agent.system_prompt import (
|
|
_tui_embedded_pane_clarifier,
|
|
build_system_prompt_parts,
|
|
)
|
|
|
|
|
|
def _stable_prompt(agent):
|
|
with (
|
|
patch("run_agent.load_soul_md", return_value=""),
|
|
patch("run_agent.build_nous_subscription_prompt", return_value=""),
|
|
patch("run_agent.build_environment_hints", return_value=""),
|
|
patch("run_agent.build_context_files_prompt", return_value=""),
|
|
):
|
|
return build_system_prompt_parts(agent)["stable"]
|
|
|
|
|
|
def _make_agent(platform="", **overrides):
|
|
base = dict(
|
|
load_soul_identity=False,
|
|
skip_context_files=False,
|
|
valid_tool_names=[],
|
|
_task_completion_guidance=False,
|
|
_tool_use_enforcement=False,
|
|
_environment_probe=False,
|
|
_kanban_worker_guidance="",
|
|
_memory_store=None,
|
|
_memory_manager=None,
|
|
_platform_hint_overrides={},
|
|
model="",
|
|
provider="",
|
|
pass_session_id=False,
|
|
session_id="",
|
|
)
|
|
base["platform"] = platform
|
|
base.update(overrides)
|
|
return SimpleNamespace(**base)
|
|
|
|
|
|
class TestDesktopHintEntry:
|
|
def test_desktop_key_exists(self):
|
|
"""The map must carry a "desktop" entry — without it the platform
|
|
hint lookup falls through to an empty string and the agent gets no
|
|
surface framing at all on the desktop chat surface."""
|
|
assert "desktop" in PLATFORM_HINTS
|
|
|
|
def test_desktop_hint_disambiguates_from_terminal(self):
|
|
"""The agent must be told it is in a graphical chat surface, NOT a
|
|
terminal. This is the line that kills the contradiction with the
|
|
old tui mis-tag."""
|
|
hint = PLATFORM_HINTS["desktop"]
|
|
lowered = hint.lower()
|
|
assert "desktop" in lowered
|
|
assert "not a terminal" in lowered
|
|
assert "graphical chat surface" in lowered
|
|
|
|
def test_desktop_hint_advertises_markdown(self):
|
|
"""The desktop renderer supports full GFM (verified via the
|
|
Streamdown pipeline in apps/desktop). The hint must steer the
|
|
agent toward markdown, not away from it like the cli/tui hints do."""
|
|
hint = PLATFORM_HINTS["desktop"]
|
|
assert "markdown" in hint.lower()
|
|
|
|
def test_desktop_hint_advertises_media_delivery(self):
|
|
"""The desktop chat intercepts MEDIA:/abs/path like telegram — images
|
|
inline, audio/video inline players, other files as download links.
|
|
Without this line the agent falls back to the cli/tui "state the
|
|
path in text" model, which is the wrong UX for the desktop surface."""
|
|
hint = PLATFORM_HINTS["desktop"]
|
|
assert "MEDIA:" in hint
|
|
|
|
def test_desktop_hint_advertises_inline_image_urls(self):
|
|
hint = PLATFORM_HINTS["desktop"]
|
|
assert "" in hint
|
|
|
|
def test_desktop_hint_does_not_inherit_tui_cron_local_only_block(self):
|
|
"""The desktop chat surface's cron delivery semantics differ from
|
|
the standalone TUI — desktop runs its own cron ticker in-process
|
|
(hermes_cli/web_server.py under HERMES_DESKTOP=1). We deliberately
|
|
do NOT parrot the tui "LOCAL-ONLY … no live-delivery channel" block
|
|
into the desktop hint, since partially-correct cron guidance is
|
|
exactly the bug class we are fixing. Cron guidance for desktop is
|
|
deferred to a follow-up issue."""
|
|
hint = PLATFORM_HINTS["desktop"]
|
|
assert "LOCAL-ONLY" not in hint
|
|
|
|
|
|
class TestDesktopHintBlockRemoved:
|
|
"""The standalone desktop-hint block that used to live in
|
|
``build_environment_hints()`` (lines ~1130-1144) was a band-aid for the
|
|
missing ``PLATFORM_HINTS["desktop"]`` entry. Once that entry exists, the
|
|
block is dead code that competes with the platform hint's claim of
|
|
what surface the agent is on. It must be gone."""
|
|
|
|
def test_build_environment_hints_has_no_runtime_surface_line(self, monkeypatch):
|
|
monkeypatch.setenv("HERMES_DESKTOP", "1")
|
|
monkeypatch.delenv("HERMES_DESKTOP_TERMINAL", raising=False)
|
|
from agent.prompt_builder import _clear_backend_probe_cache
|
|
_clear_backend_probe_cache()
|
|
hints = build_environment_hints()
|
|
assert "Runtime surface:" not in hints
|
|
assert "desktop GUI app" not in hints
|
|
|
|
def test_build_environment_hints_has_no_embedded_pane_clarifier(self, monkeypatch):
|
|
"""The ⌥-drag / ⌘+L embedded-pane clarifier moves to the platform-hint
|
|
resolution site (system_prompt.py), not build_environment_hints()."""
|
|
monkeypatch.setenv("HERMES_DESKTOP", "1")
|
|
monkeypatch.setenv("HERMES_DESKTOP_TERMINAL", "1")
|
|
from agent.prompt_builder import _clear_backend_probe_cache
|
|
_clear_backend_probe_cache()
|
|
hints = build_environment_hints()
|
|
assert "embedded terminal pane" not in hints
|
|
assert "Shift-drag" not in hints
|
|
|
|
|
|
class TestPlatformHintResolutionInStablePrompt:
|
|
"""End-to-end through ``build_system_prompt_parts`` — the platform tag on
|
|
the agent drives BOTH which PLATFORM_HINTS entry gets appended AND
|
|
whether the embedded-pane clarifier follows it. The desktop-hint block
|
|
that used to live in ``build_environment_hints()`` is gone."""
|
|
|
|
def test_desktop_platform_yields_desktop_hint_no_tui_framing(self, monkeypatch):
|
|
monkeypatch.setenv("HERMES_DESKTOP", "1")
|
|
monkeypatch.delenv("HERMES_DESKTOP_TERMINAL", raising=False)
|
|
stable = _stable_prompt(_make_agent(platform="desktop"))
|
|
assert PLATFORM_HINTS["desktop"] in stable
|
|
assert "terminal UI" not in stable
|
|
assert "Runtime surface:" not in stable
|
|
assert "embedded terminal pane" not in stable
|
|
|
|
def test_standalone_tui_yields_plain_tui_hint_no_clarifier(self, monkeypatch):
|
|
monkeypatch.delenv("HERMES_DESKTOP", raising=False)
|
|
monkeypatch.delenv("HERMES_DESKTOP_TERMINAL", raising=False)
|
|
stable = _stable_prompt(_make_agent(platform="tui"))
|
|
assert PLATFORM_HINTS["tui"] in stable
|
|
assert "embedded terminal pane" not in stable
|
|
|
|
def test_embedded_tui_yields_tui_hint_with_clarifier(self, monkeypatch):
|
|
monkeypatch.setenv("HERMES_DESKTOP", "1")
|
|
monkeypatch.setenv("HERMES_DESKTOP_TERMINAL", "1")
|
|
stable = _stable_prompt(_make_agent(platform="tui"))
|
|
assert PLATFORM_HINTS["tui"] in stable
|
|
assert "embedded terminal pane" in stable
|
|
assert "Shift-drag" in stable or "Option-drag" in stable or "⌥" in stable
|
|
|
|
def test_embedded_clarifier_does_not_attach_to_desktop_platform(self, monkeypatch):
|
|
"""Critical regression: even when HERMES_DESKTOP_TERMINAL=1, a
|
|
desktop-tagged session must NOT get the embedded-pane clarifier —
|
|
the clarifier describes the *embedded terminal pane*, which a
|
|
desktop chat session is not."""
|
|
monkeypatch.setenv("HERMES_DESKTOP", "1")
|
|
monkeypatch.setenv("HERMES_DESKTOP_TERMINAL", "1")
|
|
stable = _stable_prompt(_make_agent(platform="desktop"))
|
|
assert "embedded terminal pane" not in stable
|
|
|
|
|
|
class TestEmbeddedTuiPaneClarifier:
|
|
"""When ``HERMES_DESKTOP_TERMINAL=1``, a standalone ``hermes --tui`` is
|
|
running inside the desktop's embedded terminal pane. The user can
|
|
⌥-drag-select its output and ⌘/Ctrl+L to send it to the chat composer.
|
|
That clarifier must be appended to the ``tui`` platform hint at the
|
|
resolution site, NOT baked into the static ``PLATFORM_HINTS["tui"]``
|
|
string (which is shared with every standalone TUI session and must
|
|
stay byte-stable)."""
|
|
|
|
def test_tui_standalone_hint_byte_stable_without_env(self, monkeypatch):
|
|
"""Without HERMES_DESKTOP_TERMINAL, the clarifier is a no-op and the
|
|
resolved tui hint is exactly the static PLATFORM_HINTS["tui"]
|
|
string. Cache-stable for every standalone TUI session."""
|
|
monkeypatch.delenv("HERMES_DESKTOP_TERMINAL", raising=False)
|
|
out = _tui_embedded_pane_clarifier(PLATFORM_HINTS["tui"])
|
|
assert out == PLATFORM_HINTS["tui"]
|
|
|
|
def test_embedded_pane_clarifier_appended_when_env_set(self, monkeypatch):
|
|
monkeypatch.setenv("HERMES_DESKTOP_TERMINAL", "1")
|
|
out = _tui_embedded_pane_clarifier(PLATFORM_HINTS["tui"])
|
|
assert out.startswith(PLATFORM_HINTS["tui"])
|
|
assert "embedded terminal pane" in out
|
|
assert "Shift-drag" in out or "Option-drag" in out or "⌥" in out
|
|
|
|
def test_embedded_pane_clarifier_idempotent(self, monkeypatch):
|
|
"""Calling the clarifier twice must NOT double-append the sentence.
|
|
Cache-stability: the resolver is called once per session build, so
|
|
re-applying on an already-augmented hint is a no-op."""
|
|
monkeypatch.setenv("HERMES_DESKTOP_TERMINAL", "1")
|
|
once = _tui_embedded_pane_clarifier(PLATFORM_HINTS["tui"])
|
|
twice = _tui_embedded_pane_clarifier(once)
|
|
assert once == twice
|
|
|
|
def test_embedded_pane_clarifier_does_not_touch_empty_hint(self, monkeypatch):
|
|
"""Defensive: if the tui hint is somehow empty (e.g. overridden to
|
|
empty by config), do not synthesize a clarifier-only hint — that
|
|
would put a desktop-pane reference in the prompt without the tui
|
|
surface framing it sits under."""
|
|
monkeypatch.setenv("HERMES_DESKTOP_TERMINAL", "1")
|
|
out = _tui_embedded_pane_clarifier("")
|
|
assert out == ""
|
|
|
|
@pytest.mark.parametrize("val", ["0", "false", "no", "", "0", "False"])
|
|
def test_falsy_env_does_not_trigger_clarifier(self, monkeypatch, val):
|
|
monkeypatch.setenv("HERMES_DESKTOP_TERMINAL", val)
|
|
out = _tui_embedded_pane_clarifier(PLATFORM_HINTS["tui"])
|
|
assert out == PLATFORM_HINTS["tui"], (
|
|
f"HERMES_DESKTOP_TERMINAL={val!r} should not trigger clarifier"
|
|
)
|
|
|
|
|
|
class TestContradictionGone:
|
|
"""The original contradiction: a single assembled system prompt
|
|
contained both ``You are running in the Hermes terminal UI (TUI).`` and
|
|
``Runtime surface: you're running inside the Hermes desktop GUI app.``.
|
|
After the fix, no single session's prompt can carry both."""
|
|
|
|
def test_desktop_chat_session_has_no_tui_framing(self, monkeypatch):
|
|
monkeypatch.setenv("HERMES_DESKTOP", "1")
|
|
monkeypatch.delenv("HERMES_DESKTOP_TERMINAL", raising=False)
|
|
assert "tui" in PLATFORM_HINTS
|
|
assert "desktop" in PLATFORM_HINTS
|
|
desktop_hint = PLATFORM_HINTS["desktop"]
|
|
tui_hint = PLATFORM_HINTS["tui"]
|
|
assert "terminal UI" not in desktop_hint
|
|
assert "terminal UI" in tui_hint
|
|
|
|
def test_tui_hint_does_not_carry_desktop_marker(self):
|
|
tui_hint = PLATFORM_HINTS["tui"]
|
|
assert "desktop GUI app" not in tui_hint
|
|
assert "Runtime surface:" not in tui_hint
|