feat(memory): wire MemoryManager into run_agent.py

Adds 8 integration points for the external memory provider plugin,
all purely additive (zero existing code modified):

1. Init (~L1130): Create MemoryManager, find matching plugin provider
   from memory.provider config, initialize with session context
2. Tool injection (~L1160): Append provider tool schemas to self.tools
   and self.valid_tool_names after memory_manager init
3. System prompt (~L2705): Add external provider's system_prompt_block
   alongside existing MEMORY.md/USER.md blocks
4. Tool routing (~L5362): Route provider tool calls through
   memory_manager.handle_tool_call() before the catchall handler
5. Memory write bridge (~L5353): Notify external provider via
   on_memory_write() when the built-in memory tool writes
6. Pre-compress (~L5233): Call on_pre_compress() before context
   compression discards messages
7. Prefetch (~L6421): Inject provider prefetch results into the
   current-turn user message (same pattern as Honcho turn context)
8. Turn sync + session end (~L8161, ~L8172): sync_all() after each
   completed turn, queue_prefetch_all() for next turn, on_session_end()
   + shutdown_all() at conversation end

All hooks are wrapped in try/except — a failing provider never breaks
the agent. The existing memory system, Honcho integration, and all
other code paths are completely untouched.

Full suite: 7222 passed, 4 pre-existing failures.
This commit is contained in:
Teknium
2026-03-30 22:15:43 -07:00
parent 0b9f2ff1e8
commit dc583687ab

View File

@@ -1128,6 +1128,44 @@ class AIAgent:
self._user_profile_enabled = False
logger.debug("peer %s memory_mode=honcho: local USER.md writes disabled", _hcfg.peer_name or "user")
# Memory provider plugin (external — one at a time, alongside built-in)
# Reads memory.provider from config to select which plugin to activate.
self._memory_manager = None
if not skip_memory:
try:
_mem_provider_name = mem_config.get("provider", "") if mem_config else ""
if _mem_provider_name:
from agent.memory_manager import MemoryManager as _MemoryManager
from hermes_cli.plugins import get_plugin_memory_providers as _get_mem_providers
self._memory_manager = _MemoryManager()
for _mp in _get_mem_providers():
if _mp.name == _mem_provider_name and _mp.is_available():
self._memory_manager.add_provider(_mp)
break
if self._memory_manager.providers:
from hermes_constants import get_hermes_home as _ghh
self._memory_manager.initialize_all(
session_id=self.session_id,
platform=platform or "cli",
hermes_home=str(_ghh()),
)
logger.info("Memory provider '%s' activated", _mem_provider_name)
else:
logger.debug("Memory provider '%s' not found or not available", _mem_provider_name)
self._memory_manager = None
except Exception as _mpe:
logger.warning("Memory provider plugin init failed: %s", _mpe)
self._memory_manager = None
# Inject memory provider tool schemas into the tool surface
if self._memory_manager and self.tools is not None:
for _schema in self._memory_manager.get_all_tool_schemas():
_wrapped = {"type": "function", "function": _schema}
self.tools.append(_wrapped)
_tname = _schema.get("name", "")
if _tname:
self.valid_tool_names.add(_tname)
# Skills config: nudge interval for skill creation reminders
self._skill_nudge_interval = 10
try:
@@ -2673,6 +2711,15 @@ class AIAgent:
if user_block:
prompt_parts.append(user_block)
# External memory provider system prompt block (additive to built-in)
if self._memory_manager:
try:
_ext_mem_block = self._memory_manager.build_system_prompt()
if _ext_mem_block:
prompt_parts.append(_ext_mem_block)
except Exception:
pass
has_skills_tools = any(name in self.valid_tool_names for name in ['skills_list', 'skill_view', 'skill_manage'])
if has_skills_tools:
avail_toolsets = {
@@ -5183,6 +5230,13 @@ class AIAgent:
# Pre-compression memory flush: let the model save memories before they're lost
self.flush_memories(messages, min_turns=0)
# Notify external memory provider before compression discards context
if self._memory_manager:
try:
self._memory_manager.on_pre_compress(messages)
except Exception:
pass
compressed = self.context_compressor.compress(messages, current_tokens=approx_tokens)
todo_snapshot = self._todo_store.format_for_injection()
@@ -5303,7 +5357,19 @@ class AIAgent:
# Also send user observations to Honcho when active
if self._honcho and target == "user" and function_args.get("action") == "add":
self._honcho_save_user_observation(function_args.get("content", ""))
# Bridge: notify external memory provider of built-in memory writes
if self._memory_manager and function_args.get("action") in ("add", "replace"):
try:
self._memory_manager.on_memory_write(
function_args.get("action", ""),
target,
function_args.get("content", ""),
)
except Exception:
pass
return result
elif self._memory_manager and self._memory_manager.has_tool(function_name):
return self._memory_manager.handle_tool_call(function_name, function_args)
elif function_name == "clarify":
from tools.clarify_tool import clarify_tool as _clarify_tool
return _clarify_tool(
@@ -6352,6 +6418,19 @@ class AIAgent:
api_msg.get("content", ""), self._honcho_turn_context
)
# External memory provider prefetch: inject alongside Honcho context
if idx == current_turn_user_idx and msg.get("role") == "user" and self._memory_manager:
try:
_ext_prefetch = self._memory_manager.prefetch_all(
api_msg.get("content", "") if isinstance(api_msg.get("content"), str) else ""
)
if _ext_prefetch:
_base = api_msg.get("content", "")
if isinstance(_base, str):
api_msg["content"] = _base + "\n\n" + _ext_prefetch
except Exception:
pass
# For ALL assistant messages, pass reasoning back to the API
# This ensures multi-turn reasoning context is preserved
if msg.get("role") == "assistant":
@@ -8078,6 +8157,14 @@ class AIAgent:
_should_review_skills = True
self._iters_since_skill = 0
# External memory provider: sync the completed turn + queue next prefetch
if self._memory_manager and final_response and user_message:
try:
self._memory_manager.sync_all(user_message, final_response)
self._memory_manager.queue_prefetch_all(user_message)
except Exception:
pass
# Background memory/skill review — runs AFTER the response is delivered
# so it never competes with the user's task for model attention.
if final_response and not interrupted and (_should_review_memory or _should_review_skills):
@@ -8090,6 +8177,14 @@ class AIAgent:
except Exception:
pass # Background review is best-effort
# Memory provider: session end + shutdown
if self._memory_manager:
try:
self._memory_manager.on_session_end(messages)
self._memory_manager.shutdown_all()
except Exception:
pass
# Plugin hook: on_session_end
# Fired at the very end of every run_conversation call.
# Plugins can use this for cleanup, flushing buffers, etc.