mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-08 03:01:47 +00:00
fix(gateway): preview memory prefetch context in chat
This commit is contained in:
parent
13038dc747
commit
2a1e0fc205
8 changed files with 508 additions and 9 deletions
|
|
@ -202,6 +202,20 @@ class TestMemoryManager:
|
|||
assert p1.prefetch_queries == ["what do you know?"]
|
||||
assert p2.prefetch_queries == ["what do you know?"]
|
||||
|
||||
def test_prefetch_all_details_returns_provider_names(self):
|
||||
mgr = MemoryManager()
|
||||
p1 = FakeMemoryProvider("builtin")
|
||||
p1._prefetch_result = "Memory from builtin"
|
||||
p2 = FakeMemoryProvider("hindsight")
|
||||
p2._prefetch_result = "Memory from hindsight"
|
||||
mgr.add_provider(p1)
|
||||
mgr.add_provider(p2)
|
||||
|
||||
text, providers = mgr.prefetch_all_details("what do you know?")
|
||||
|
||||
assert text == "Memory from builtin\n\nMemory from hindsight"
|
||||
assert providers == ["builtin", "hindsight"]
|
||||
|
||||
def test_prefetch_skips_empty(self):
|
||||
mgr = MemoryManager()
|
||||
p1 = FakeMemoryProvider("builtin")
|
||||
|
|
|
|||
|
|
@ -54,6 +54,23 @@ class TestResolveDisplaySetting:
|
|||
# Unknown platform, no config → global default "all"
|
||||
assert resolve_display_setting(config, "unknown_platform", "tool_progress") == "all"
|
||||
|
||||
def test_memory_context_defaults_to_preview_for_all_platforms(self):
|
||||
"""Memory context defaults to bounded preview everywhere for dogfood visibility."""
|
||||
from gateway.display_config import resolve_display_setting
|
||||
|
||||
config = {}
|
||||
for plat in (
|
||||
"discord",
|
||||
"telegram",
|
||||
"tui",
|
||||
"api_server",
|
||||
"webhook",
|
||||
"email",
|
||||
"unknown_platform",
|
||||
):
|
||||
assert resolve_display_setting(config, plat, "memory_context") == "preview", plat
|
||||
assert resolve_display_setting(config, plat, "memory_context_max_chars") == 1200, plat
|
||||
|
||||
def test_fallback_parameter_used_last(self):
|
||||
"""Explicit fallback is used when nothing else matches."""
|
||||
from gateway.display_config import resolve_display_setting
|
||||
|
|
@ -170,6 +187,28 @@ class TestYAMLNormalisation:
|
|||
config = {"display": {"platforms": {"slack": {"tool_progress": False}}}}
|
||||
assert resolve_display_setting(config, "slack", "tool_progress") == "off"
|
||||
|
||||
def test_memory_context_modes_normalised(self):
|
||||
"""Memory context mode values are normalised and validated."""
|
||||
from gateway.display_config import resolve_display_setting
|
||||
|
||||
assert resolve_display_setting(
|
||||
{"display": {"memory_context": False}}, "discord", "memory_context"
|
||||
) == "off"
|
||||
assert resolve_display_setting(
|
||||
{"display": {"memory_context": True}}, "discord", "memory_context"
|
||||
) == "preview"
|
||||
assert resolve_display_setting(
|
||||
{"display": {"memory_context": "SUMMARY"}}, "discord", "memory_context"
|
||||
) == "summary"
|
||||
assert resolve_display_setting(
|
||||
{"display": {"memory_context": "nonsense"}}, "discord", "memory_context"
|
||||
) == "preview"
|
||||
assert resolve_display_setting(
|
||||
{"display": {"memory_context_max_chars": "80"}},
|
||||
"discord",
|
||||
"memory_context_max_chars",
|
||||
) == 80
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Built-in platform defaults (tier system)
|
||||
|
|
|
|||
|
|
@ -497,6 +497,67 @@ class VerboseAgent:
|
|||
}
|
||||
|
||||
|
||||
class MemoryPrefetchAgent:
|
||||
"""Agent that emits Hindsight-style memory prefetch progress."""
|
||||
|
||||
PREVIEW = (
|
||||
'# Hindsight Memory (persistent cross-session context)\n\n'
|
||||
'Use this to answer questions about the user and prior sessions. Do not call tools to look up information that is already present\n\n'
|
||||
'here.\n\n'
|
||||
'* [{"role": "user", "content": "User: [jim] [] Hindsight health is green\\n[ ] memory.prefetch emits exactly once", '
|
||||
'"timestamp": "2026-04-24T20:30:36.770766+00:00"}, '
|
||||
'{"role": "assistant", "content": "Assistant: Updated current checklist:\\n\\n```md\\n[x] Hindsight health is green\\n```", '
|
||||
'"timestamp": "2026-04-24T20:31:00.000000+00:00"}]'
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.tool_progress_callback = kwargs.get("tool_progress_callback")
|
||||
self.tools = []
|
||||
|
||||
def run_conversation(self, message, conversation_history=None, task_id=None):
|
||||
self.tool_progress_callback(
|
||||
"memory.prefetch",
|
||||
None,
|
||||
self.PREVIEW,
|
||||
None,
|
||||
providers=["hindsight"],
|
||||
chars=14528,
|
||||
)
|
||||
time.sleep(0.35)
|
||||
return {
|
||||
"final_response": "done",
|
||||
"messages": [],
|
||||
"api_calls": 1,
|
||||
}
|
||||
|
||||
|
||||
class RawMemoryContextPrefetchAgent(MemoryPrefetchAgent):
|
||||
"""Agent that emits the raw injected wrapper shape seen in Discord dogfood."""
|
||||
|
||||
PREVIEW = (
|
||||
'<memory-context>\n'
|
||||
'[System note: The following is recalled memory context, NOT new user input. '
|
||||
'Treat as informational background data.]\n\n'
|
||||
'# Hindsight Memory (persistent cross-session context)\n'
|
||||
'Use this to answer questions about the user and prior sessions. Do not call tools to look up information that is already present here.\n\n'
|
||||
'- [[{"role": "user", "content": "User: [jim] this is what im seeing", '
|
||||
'"timestamp": "2026-04-24T21:25:35.131663+00:00"}]]\n'
|
||||
'</memory-context>'
|
||||
)
|
||||
|
||||
|
||||
class PlainTranscriptMemoryPrefetchAgent(MemoryPrefetchAgent):
|
||||
"""Agent that emits plain Hindsight transcript lines without JSON payloads."""
|
||||
|
||||
PREVIEW = (
|
||||
'[System note: The following is recalled memory context, NOT new user input.]\n'
|
||||
'# Hindsight Memory (persistent cross-session context)\n'
|
||||
'Use this to answer questions about the user and prior sessions.\n'
|
||||
'Assistant: Live dashboard is up. Tailnet link is ready.\n'
|
||||
'Assistant: hi jim. Ready.'
|
||||
)
|
||||
|
||||
|
||||
async def _run_with_agent(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
|
|
@ -708,6 +769,110 @@ async def test_run_agent_previewed_final_marks_already_sent(monkeypatch, tmp_pat
|
|||
assert [call["content"] for call in adapter.sent] == ["You're welcome."]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_discord_memory_prefetch_preview_uses_markdown_not_raw_json(monkeypatch, tmp_path):
|
||||
adapter, result = await _run_with_agent(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
MemoryPrefetchAgent,
|
||||
session_id="sess-memory-prefetch-discord",
|
||||
config_data={"display": {"memory_context": "preview", "memory_context_max_chars": 700}},
|
||||
platform=Platform.DISCORD,
|
||||
chat_id="discord-1",
|
||||
chat_type="dm",
|
||||
thread_id=None,
|
||||
)
|
||||
|
||||
assert result["final_response"] == "done"
|
||||
all_content = "\n".join(call["content"] for call in adapter.sent)
|
||||
all_content += "\n".join(call["content"] for call in adapter.edits)
|
||||
assert "🧠 **Loaded memory context from hindsight** (14528 chars)" in all_content
|
||||
assert "> 1. Hindsight health is green" in all_content
|
||||
assert "> 2. Updated current checklist:" in all_content
|
||||
assert "**User**:" not in all_content
|
||||
assert "**Assistant**:" not in all_content
|
||||
assert "[jim]" not in all_content
|
||||
assert '"role"' not in all_content
|
||||
assert '"content"' not in all_content
|
||||
assert "Use this to answer questions" not in all_content
|
||||
assert "```" not in all_content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_discord_memory_prefetch_preview_hides_raw_memory_context_wrapper(monkeypatch, tmp_path):
|
||||
adapter, result = await _run_with_agent(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
RawMemoryContextPrefetchAgent,
|
||||
session_id="sess-memory-prefetch-discord-wrapper",
|
||||
config_data={"display": {"memory_context": "preview", "memory_context_max_chars": 700}},
|
||||
platform=Platform.DISCORD,
|
||||
chat_id="discord-1",
|
||||
chat_type="dm",
|
||||
thread_id=None,
|
||||
)
|
||||
|
||||
assert result["final_response"] == "done"
|
||||
all_content = "\n".join(call["content"] for call in adapter.sent)
|
||||
all_content += "\n".join(call["content"] for call in adapter.edits)
|
||||
assert "🧠 **Loaded memory context from hindsight** (14528 chars)" in all_content
|
||||
assert "> 1. this is what im seeing" in all_content
|
||||
assert "**User**:" not in all_content
|
||||
assert "[jim]" not in all_content
|
||||
assert "<memory-context>" not in all_content
|
||||
assert "</memory-context>" not in all_content
|
||||
assert "[System note:" not in all_content
|
||||
assert "# Hindsight Memory" not in all_content
|
||||
assert "Use this to answer questions" not in all_content
|
||||
assert '"role"' not in all_content
|
||||
assert '"content"' not in all_content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_discord_memory_prefetch_preview_formats_plain_hindsight_transcript(monkeypatch, tmp_path):
|
||||
adapter, result = await _run_with_agent(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
PlainTranscriptMemoryPrefetchAgent,
|
||||
session_id="sess-memory-prefetch-discord-plain",
|
||||
config_data={"display": {"memory_context": "preview", "memory_context_max_chars": 700}},
|
||||
platform=Platform.DISCORD,
|
||||
chat_id="discord-1",
|
||||
chat_type="dm",
|
||||
thread_id=None,
|
||||
)
|
||||
|
||||
assert result["final_response"] == "done"
|
||||
all_content = "\n".join(call["content"] for call in adapter.sent)
|
||||
all_content += "\n".join(call["content"] for call in adapter.edits)
|
||||
assert "> 1. Live dashboard is up. Tailnet link is ready." in all_content
|
||||
assert "> 2. hi jim. Ready." in all_content
|
||||
assert "Assistant:" not in all_content
|
||||
assert "# Hindsight Memory" not in all_content
|
||||
assert "Use this to answer questions" not in all_content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_memory_prefetch_progress_is_discord_only(monkeypatch, tmp_path):
|
||||
adapter, result = await _run_with_agent(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
MemoryPrefetchAgent,
|
||||
session_id="sess-memory-prefetch-telegram",
|
||||
config_data={"display": {"memory_context": "preview", "memory_context_max_chars": 700}},
|
||||
platform=Platform.TELEGRAM,
|
||||
chat_id="-1001",
|
||||
chat_type="group",
|
||||
thread_id="17585",
|
||||
)
|
||||
|
||||
assert result["final_response"] == "done"
|
||||
all_content = "\n".join(call["content"] for call in adapter.sent)
|
||||
all_content += "\n".join(call["content"] for call in adapter.edits)
|
||||
assert "Loaded memory context" not in all_content
|
||||
assert "Hindsight health is green" not in all_content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_agent_matrix_streaming_omits_cursor(monkeypatch, tmp_path):
|
||||
adapter, result = await _run_with_agent(
|
||||
|
|
|
|||
|
|
@ -4771,6 +4771,110 @@ class TestMemoryContextSanitization:
|
|||
assert "stale observation" not in result
|
||||
assert "how is the honcho working" in result
|
||||
|
||||
def test_memory_prefetch_emits_progress_with_provider_names(self, agent):
|
||||
raw_memory = (
|
||||
"# Hindsight Memory (persistent cross-session context)\n"
|
||||
"Use this to answer questions about the user and prior sessions.\n\n"
|
||||
'- [{"role": "user", "content": "User: [jim] prior useful context"}]\n'
|
||||
)
|
||||
|
||||
class FakeMemoryManager:
|
||||
def on_turn_start(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
def prefetch_all_details(self, query, *, session_id=""):
|
||||
assert query == "hello"
|
||||
return raw_memory, ["hindsight"]
|
||||
|
||||
def sync_all(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
def queue_prefetch_all(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
events = []
|
||||
|
||||
def capture_progress(*args, **kwargs):
|
||||
events.append((args, kwargs))
|
||||
|
||||
agent._cached_system_prompt = "You are helpful."
|
||||
agent._use_prompt_caching = False
|
||||
agent.tool_delay = 0
|
||||
agent.compression_enabled = False
|
||||
agent.save_trajectories = False
|
||||
agent._memory_manager = FakeMemoryManager()
|
||||
agent.tool_progress_callback = capture_progress
|
||||
agent.client.chat.completions.create.return_value = _mock_response(
|
||||
content="Final answer",
|
||||
finish_reason="stop",
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(agent, "_persist_session"),
|
||||
patch.object(agent, "_save_trajectory"),
|
||||
patch.object(agent, "_cleanup_task_resources"),
|
||||
):
|
||||
result = agent.run_conversation("hello")
|
||||
|
||||
assert result["final_response"] == "Final answer"
|
||||
sent_messages = agent.client.chat.completions.create.call_args.kwargs["messages"]
|
||||
assert "<memory-context>" in sent_messages[-1]["content"]
|
||||
assert "prior useful context" in sent_messages[-1]["content"]
|
||||
memory_events = [(args, kwargs) for args, kwargs in events if args[0] == "memory.prefetch"]
|
||||
assert len(memory_events) == 1
|
||||
args, kwargs = memory_events[0]
|
||||
assert args[:2] == ("memory.prefetch", "memory")
|
||||
assert "prior useful context" in args[2]
|
||||
assert kwargs["providers"] == ["hindsight"]
|
||||
assert kwargs["provider_count"] == 1
|
||||
assert kwargs["chars"] == len(raw_memory)
|
||||
assert kwargs["injected"] is True
|
||||
|
||||
def test_memory_prefetch_progress_only_emits_when_context_block_injected(self, agent):
|
||||
raw_memory = "memory text"
|
||||
|
||||
class FakeMemoryManager:
|
||||
def on_turn_start(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
def prefetch_all_details(self, query, *, session_id=""):
|
||||
assert query == "hello"
|
||||
return raw_memory, ["hindsight"]
|
||||
|
||||
def sync_all(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
def queue_prefetch_all(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
events = []
|
||||
|
||||
def capture_progress(*args, **kwargs):
|
||||
events.append((args, kwargs))
|
||||
|
||||
agent._cached_system_prompt = "You are helpful."
|
||||
agent._use_prompt_caching = False
|
||||
agent.tool_delay = 0
|
||||
agent.compression_enabled = False
|
||||
agent.save_trajectories = False
|
||||
agent._memory_manager = FakeMemoryManager()
|
||||
agent.tool_progress_callback = capture_progress
|
||||
agent.client.chat.completions.create.return_value = _mock_response(
|
||||
content="Final answer",
|
||||
finish_reason="stop",
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(run_agent, "build_memory_context_block", return_value=""),
|
||||
patch.object(agent, "_persist_session"),
|
||||
patch.object(agent, "_save_trajectory"),
|
||||
patch.object(agent, "_cleanup_task_resources"),
|
||||
):
|
||||
result = agent.run_conversation("hello")
|
||||
|
||||
assert result["final_response"] == "Final answer"
|
||||
assert [args for args, _kwargs in events if args[0] == "memory.prefetch"] == []
|
||||
|
||||
|
||||
class TestMemoryProviderTurnStart:
|
||||
"""run_conversation() must call memory_manager.on_turn_start() before prefetch_all().
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue