Compare commits

...

3 Commits

Author SHA1 Message Date
00a8221088 feat: integrate mempalace archival into background review 2026-04-08 10:14:38 +00:00
ec501b7d7b feat: register mempalace_tool in model_tools
All checks were successful
Forge CI / smoke-and-build (pull_request) Successful in 51s
2026-04-08 09:58:17 +00:00
41b69d63a8 feat: add mempalace_tool 2026-04-08 09:58:15 +00:00
3 changed files with 170 additions and 7 deletions

View File

@@ -158,6 +158,7 @@ def _discover_tools():
"tools.send_message_tool",
# "tools.honcho_tools", # Removed — Honcho is now a memory provider plugin
"tools.homeassistant_tool",
"tools.mempalace_tool",
]
import importlib
for mod_name in _modules:

View File

@@ -1027,6 +1027,7 @@ class AIAgent:
self._memory_nudge_interval = 10
self._memory_flush_min_turns = 6
self._turns_since_memory = 0
self._turns_since_palace = 0
self._iters_since_skill = 0
if not skip_memory:
try:
@@ -1035,6 +1036,7 @@ class AIAgent:
self._user_profile_enabled = mem_config.get("user_profile_enabled", False)
self._memory_nudge_interval = int(mem_config.get("nudge_interval", 10))
self._memory_flush_min_turns = int(mem_config.get("flush_min_turns", 6))
self._palace_nudge_interval = int(mem_config.get("palace_nudge_interval", 15))
if self._memory_enabled or self._user_profile_enabled:
from tools.memory_tool import MemoryStore
self._memory_store = MemoryStore(
@@ -1797,6 +1799,16 @@ class AIAgent:
"If nothing is worth saving, just say 'Nothing to save.' and stop."
)
_PALACE_REVIEW_PROMPT = (
"Review the conversation above and archive a summary of the key events, "
"decisions, and outcomes to the MemPalace.\n\n"
"This ensures that future sessions can recall the context of this work "
"even after the current session history is lost.\n\n"
"Use the mempalace tool with action='record' and room='nexus' or 'workspace'. "
"Include a concise but informative summary of what was achieved."
)
_COMBINED_REVIEW_PROMPT = (
"Review the conversation above and consider two things:\n\n"
"**Memory**: Has the user revealed things about themselves — their persona, "
@@ -1816,6 +1828,7 @@ class AIAgent:
messages_snapshot: List[Dict],
review_memory: bool = False,
review_skills: bool = False,
review_palace: bool = False,
) -> None:
"""Spawn a background thread to review the conversation for memory/skill saves.
@@ -1826,13 +1839,20 @@ class AIAgent:
"""
import threading
# Pick the right prompt based on which triggers fired
if review_memory and review_skills:
prompt = self._COMBINED_REVIEW_PROMPT
elif review_memory:
prompt = self._MEMORY_REVIEW_PROMPT
else:
prompt = self._SKILL_REVIEW_PROMPT
prompts = []
if review_memory: prompts.append(self._MEMORY_REVIEW_PROMPT)
if review_skills: prompts.append(self._SKILL_REVIEW_PROMPT)
if review_palace: prompts.append(self._PALACE_REVIEW_PROMPT)
if not prompts:
return
prompt = "\n\n".join(prompts)
if len(prompts) > 1:
prompt = "Review the conversation above and perform the following tasks:\n\n" + prompt
def _run_review():
import contextlib, os as _os
@@ -6151,6 +6171,13 @@ class AIAgent:
# Reset nudge counters
if function_name == "memory":
self._turns_since_memory = 0
if (self._palace_nudge_interval > 0
and "mempalace" in self.valid_tool_names):
self._turns_since_palace += 1
if self._turns_since_palace >= self._palace_nudge_interval:
_should_review_palace = True
self._turns_since_palace = 0
elif function_name == "skill_manage":
self._iters_since_skill = 0
@@ -7002,6 +7029,7 @@ class AIAgent:
# Skill trigger is checked AFTER the agent loop completes, based on
# how many tool iterations THIS turn used.
_should_review_memory = False
_should_review_palace = False
if (self._memory_nudge_interval > 0
and "memory" in self.valid_tool_names
and self._memory_store):
@@ -9155,12 +9183,13 @@ class AIAgent:
# 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):
if final_response and not interrupted and (_should_review_memory or _should_review_skills or _should_review_palace):
try:
self._spawn_background_review(
messages_snapshot=list(messages),
review_memory=_should_review_memory,
review_skills=_should_review_skills,
review_palace=_should_review_palace,
)
except Exception:
pass # Background review is best-effort

133
tools/mempalace_tool.py Normal file
View File

@@ -0,0 +1,133 @@
"""
mempalace_tool.py — Interface for the MemPalace vector memory system.
This tool allows the agent to search and record memories in a ChromaDB-backed
vector database via the fleet_api.
"""
import json
import os
import requests
import logging
from typing import Dict, Any, List, Optional
from tools.registry import registry
logger = logging.getLogger(__name__)
# Default to the fleet_api port defined in the-nexus
MEMPALACE_API_URL = os.environ.get("MEMPALACE_API_URL", "http://127.0.0.1:7771")
def mempalace_tool(
action: str,
query: str = "",
text: str = "",
room: Optional[str] = None,
wing: Optional[str] = None,
n_results: int = 10,
metadata: Optional[Dict[str, Any]] = None,
) -> str:
"""
Interface for the MemPalace vector memory system.
Actions:
search: Search the palace for relevant memories.
record: Add a new memory to the palace.
wings: List available wizard wings.
"""
try:
if action == "search":
if not query:
return json.dumps({"success": False, "error": "Query is required for search."})
params = {"q": query, "n": n_results}
if room:
params["room"] = room
response = requests.get(f"{MEMPALACE_API_URL}/search", params=params, timeout=10)
response.raise_for_status()
return json.dumps({"success": True, "data": response.json()})
elif action == "record":
if not text:
return json.dumps({"success": False, "error": "Text is required for record."})
payload = {
"text": text,
"room": room or "general",
"wing": wing,
"metadata": metadata or {}
}
response = requests.post(f"{MEMPALACE_API_URL}/record", json=payload, timeout=10)
response.raise_for_status()
return json.dumps({"success": True, "data": response.json()})
elif action == "wings":
response = requests.get(f"{MEMPALACE_API_URL}/wings", timeout=10)
response.raise_for_status()
return json.dumps({"success": True, "data": response.json()})
else:
return json.dumps({"success": False, "error": f"Unknown action: {action}"})
except requests.exceptions.ConnectionError:
return json.dumps({
"success": False,
"error": f"Could not connect to MemPalace API at {MEMPALACE_API_URL}. Ensure fleet_api.py is running."
})
except Exception as e:
logger.error(f"MemPalace tool error: {e}")
return json.dumps({"success": False, "error": str(e)})
MEMPALACE_SCHEMA = {
"name": "mempalace",
"description": (
"Search or record memories in the MemPalace vector database. "
"Use this for long-term, high-volume memory that exceeds the curated memory limits. "
"The palace is organized into 'rooms' (e.g., forge, hermes, nexus, issues, experiments)."
),
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["search", "record", "wings"],
"description": "The action to perform."
},
"query": {
"type": "string",
"description": "The search query (required for 'search')."
},
"text": {
"type": "string",
"description": "The memory text to record (required for 'record')."
},
"room": {
"type": "string",
"description": "Optional room filter or target room (e.g., forge, hermes, nexus, issues, experiments)."
},
"wing": {
"type": "string",
"description": "Optional wizard wing filter."
},
"n_results": {
"type": "integer",
"default": 10,
"description": "Maximum number of results to return."
},
"metadata": {
"type": "object",
"description": "Optional metadata for the memory (only for 'record')."
}
},
"required": ["action"]
}
}
registry.register(
name="mempalace",
toolset="mempalace",
schema=MEMPALACE_SCHEMA,
handler=lambda args, **kw: mempalace_tool(**args),
emoji="🏛️",
)