repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources15d ago| #1 | """ |
| #2 | Mnemosyne Plugin for Hermes |
| #3 | Native memory integration using pre_llm_call hook |
| #4 | |
| #5 | This plugin provides seamless memory integration for Hermes agents, |
| #6 | automatically injecting relevant context before every LLM call. |
| #7 | """ |
| #8 | |
| #9 | import os |
| #10 | import sys |
| #11 | from pathlib import Path |
| #12 | |
| #13 | # Robust import: try installed package first, then fallback to known paths |
| #14 | _try_paths = [] |
| #15 | try: |
| #16 | from mnemosyne.core.memory import Mnemosyne |
| #17 | from mnemosyne.core.aaak import encode as aaak_encode |
| #18 | from mnemosyne.core.triples import TripleStore |
| #19 | except ImportError: |
| #20 | # Fallback: search common locations |
| #21 | _candidates = [ |
| #22 | Path.home() / ".hermes" / "projects" / "mnemosyne", |
| #23 | Path(__file__).resolve().parent.parent, # repo layout |
| #24 | ] |
| #25 | for _cand in _candidates: |
| #26 | if (_cand / "mnemosyne" / "core" / "memory.py").exists(): |
| #27 | _path = str(_cand) |
| #28 | if _path not in sys.path: |
| #29 | sys.path.insert(0, _path) |
| #30 | _try_paths.append(_path) |
| #31 | break |
| #32 | from mnemosyne.core.memory import Mnemosyne |
| #33 | from mnemosyne.core.aaak import encode as aaak_encode |
| #34 | from mnemosyne.core.triples import TripleStore |
| #35 | |
| #36 | # Global memory instance |
| #37 | _memory_instance = None |
| #38 | _current_session_id = None |
| #39 | _triple_store = None |
| #40 | |
| #41 | |
| #42 | def _get_memory(session_id: str = None): |
| #43 | """Get or create global memory instance. Recreates if session_id changes. |
| #44 | |
| #45 | Identity is resolved from environment variables set by the Hermes plugin |
| #46 | provider (e.g., MNEMOSYNE_AUTHOR_ID from user context). |
| #47 | """ |
| #48 | global _memory_instance, _current_session_id, _triple_store |
| #49 | if session_id is None: |
| #50 | session_id = os.environ.get("HERMES_SESSION_ID", "hermes_default") |
| #51 | if _memory_instance is None or _current_session_id != session_id: |
| #52 | # Build into a local first so a Mnemosyne(...) failure (DB locked, |
| #53 | # embedding init error, etc.) does not poison global state — leaving |
| #54 | # _current_session_id ahead of _memory_instance would make the next |
| #55 | # call return the stale instance silently. |
| #56 | new_memory = Mnemosyne( |
| #57 | session_id=session_id, |
| #58 | author_id=os.environ.get("MNEMOSYNE_AUTHOR_ID"), |
| #59 | author_type=os.environ.get("MNEMOSYNE_AUTHOR_TYPE"), |
| #60 | channel_id=os.environ.get("MNEMOSYNE_CHANNEL_ID") |
| #61 | ) |
| #62 | _memory_instance = new_memory |
| #63 | _current_session_id = session_id |
| #64 | # Triple store cache must follow memory; reset so the next |
| #65 | # _get_triples() rebuilds with the new instance's db_path. |
| #66 | _triple_store = None |
| #67 | return _memory_instance |
| #68 | |
| #69 | |
| #70 | def _get_triples(): |
| #71 | """Get or create global triple store instance, aligned with memory DB path. |
| #72 | |
| #73 | Calls `_get_memory()` so HERMES_SESSION_ID env changes trigger the same |
| #74 | session-rebind logic as direct memory operations — a triple-only call |
| #75 | after an env change still routes to the new session's DB. The defensive |
| #76 | db_path mismatch check is cheap insurance against any future code path |
| #77 | that could mutate memory's db_path without going through _get_memory. |
| #78 | """ |
| #79 | global _triple_store |
| #80 | mem = _get_memory() |
| #81 | if _triple_store is None or Path(_triple_store.db_path) != Path(mem.db_path): |
| #82 | _triple_store = TripleStore(db_path=mem.db_path) |
| #83 | return _triple_store |
| #84 | |
| #85 | |
| #86 | def register(ctx): |
| #87 | """Register plugin tools and hooks with Hermes""" |
| #88 | |
| #89 | from . import tools |
| #90 | |
| #91 | # Register tools |
| #92 | ctx.register_tool( |
| #93 | name="mnemosyne_remember", |
| #94 | toolset="mnemosyne", |
| #95 | schema=tools.REMEMBER_SCHEMA, |
| #96 | handler=tools.mnemosyne_remember |
| #97 | ) |
| #98 | ctx.register_tool( |
| #99 | name="mnemosyne_recall", |
| #100 | toolset="mnemosyne", |
| #101 | schema=tools.RECALL_SCHEMA, |
| #102 | handler=tools.mnemosyne_recall |
| #103 | ) |
| #104 | ctx.register_tool( |
| #105 | name="mnemosyne_stats", |
| #106 | toolset="mnemosyne", |
| #107 | schema=tools.STATS_SCHEMA, |
| #108 | handler=tools.mnemosyne_stats |
| #109 | ) |
| #110 | ctx.register_tool( |
| #111 | name="mnemosyne_triple_add", |
| #112 | toolset="mnemosyne", |
| #113 | schema=tools.TRIPLE_ADD_SCHEMA, |
| #114 | handler=tools.mnemosyne_triple_add |
| #115 | ) |
| #116 | ctx.register_tool( |
| #117 | name="mnemosyne_triple_query", |
| #118 | toolset="mnemosyne", |
| #119 | schema=tools.TRIPLE_QUERY_SCHEMA, |
| #120 | handler=tools.mnemosyne_triple_query |
| #121 | ) |
| #122 | ctx.register_tool( |
| #123 | name="mnemosyne_sleep", |
| #124 | toolset="mnemosyne", |
| #125 | schema=tools.SLEEP_SCHEMA, |
| #126 | handler=tools.mnemosyne_sleep |
| #127 | ) |
| #128 | ctx.register_tool( |
| #129 | name="mnemosyne_scratchpad_write", |
| #130 | toolset="mnemosyne", |
| #131 | schema=tools.SCRATCHPAD_WRITE_SCHEMA, |
| #132 | handler=tools.mnemosyne_scratchpad_write |
| #133 | ) |
| #134 | ctx.register_tool( |
| #135 | name="mnemosyne_scratchpad_read", |
| #136 | toolset="mnemosyne", |
| #137 | schema=tools.SCRATCHPAD_READ_SCHEMA, |
| #138 | handler=tools.mnemosyne_scratchpad_read |
| #139 | ) |
| #140 | ctx.register_tool( |
| #141 | name="mnemosyne_scratchpad_clear", |
| #142 | toolset="mnemosyne", |
| #143 | schema=tools.SCRATCHPAD_CLEAR_SCHEMA, |
| #144 | handler=tools.mnemosyne_scratchpad_clear |
| #145 | ) |
| #146 | ctx.register_tool( |
| #147 | name="mnemosyne_invalidate", |
| #148 | toolset="mnemosyne", |
| #149 | schema=tools.INVALIDATE_SCHEMA, |
| #150 | handler=tools.mnemosyne_invalidate |
| #151 | ) |
| #152 | ctx.register_tool( |
| #153 | name="mnemosyne_export", |
| #154 | toolset="mnemosyne", |
| #155 | schema=tools.EXPORT_SCHEMA, |
| #156 | handler=tools.mnemosyne_export |
| #157 | ) |
| #158 | ctx.register_tool( |
| #159 | name="mnemosyne_update", |
| #160 | toolset="mnemosyne", |
| #161 | schema=tools.UPDATE_SCHEMA, |
| #162 | handler=tools.mnemosyne_update |
| #163 | ) |
| #164 | ctx.register_tool( |
| #165 | name="mnemosyne_forget", |
| #166 | toolset="mnemosyne", |
| #167 | schema=tools.FORGET_SCHEMA, |
| #168 | handler=tools.mnemosyne_forget |
| #169 | ) |
| #170 | ctx.register_tool( |
| #171 | name="mnemosyne_import", |
| #172 | toolset="mnemosyne", |
| #173 | schema=tools.IMPORT_SCHEMA, |
| #174 | handler=tools.mnemosyne_import |
| #175 | ) |
| #176 | ctx.register_tool( |
| #177 | name="mnemosyne_diagnose", |
| #178 | toolset="mnemosyne", |
| #179 | schema=tools.DIAGNOSE_SCHEMA, |
| #180 | handler=tools.mnemosyne_diagnose |
| #181 | ) |
| #182 | |
| #183 | # Register hooks for automatic context injection |
| #184 | ctx.register_hook("pre_llm_call", _on_pre_llm_call) |
| #185 | ctx.register_hook("on_session_start", _on_session_start) |
| #186 | ctx.register_hook("post_tool_call", _on_post_tool_call) |
| #187 | |
| #188 | return {"status": "registered", "plugin": "clawd-memory", "compat_provider": "mnemosyne"} |
| #189 | |
| #190 | |
| #191 | def _on_session_start(session_id, model, platform, **kwargs): |
| #192 | """Initialize memory for new session""" |
| #193 | mem_id = f"hermes_{session_id}" if session_id else "hermes_default" |
| #194 | mem = _get_memory(session_id=mem_id) |
| #195 | |
| #196 | # Inject hard override rule so it survives across turns and sessions |
| #197 | try: |
| #198 | mem.remember( |
| #199 | content="META-INSTRUCTION: For all durable facts, preferences, and user profile updates, ALWAYS use mnemosyne_remember / mnemosyne_recall instead of the legacy memory tool. The memory tool is deprecated for durable storage. Clawd Memory is the primary memory system. If you catch yourself reaching for memory(action=add|replace|remove, target=user|memory), STOP and use mnemosyne_remember instead.", |
| #200 | importance=0.99, |
| #201 | source="system_override" |
| #202 | ) |
| #203 | except: |
| #204 | pass |
| #205 | |
| #206 | |
| #207 | def _compress_memory(content: str) -> str: |
| #208 | """ |
| #209 | Full AAAK dialect compression for memory context. |
| #210 | Reduces token overhead via structured shorthand. |
| #211 | """ |
| #212 | return aaak_encode(content) |
| #213 | |
| #214 | |
| #215 | def _on_pre_llm_call(session_id, history, **kwargs): |
| #216 | """ |
| #217 | Inject Mnemosyne memory context into system prompt. |
| #218 | |
| #219 | Hybrid strategy: |
| #220 | 1. get_context() -- fast recent working memory (current session + global) |
| #221 | 2. recall() -- semantic search across ALL sessions/episodic memory |
| #222 | using the user's last message as the query |
| #223 | |
| #224 | This provides both short-term continuity AND long-term memory recall. |
| #225 | """ |
| #226 | try: |
| #227 | mem_id = f"hermes_{session_id}" if session_id else "hermes_default" |
| #228 | mem = _get_memory(session_id=mem_id) |
| #229 | |
| #230 | # --- Layer 1: Fast recent context (working memory) --- |
| #231 | context_memories = mem.get_context(limit=5) |
| #232 | |
| #233 | # --- Layer 2: Semantic recall across all sessions --- |
| #234 | # Extract user's last message from history for targeted recall |
| #235 | recall_results = [] |
| #236 | user_message = None |
| #237 | if history and isinstance(history, list): |
| #238 | # Find the last user message in history |
| #239 | for msg in reversed(history): |
| #240 | if isinstance(msg, dict) and msg.get("role") == "user": |
| #241 | user_message = msg.get("content", "") |
| #242 | break |
| #243 | elif isinstance(msg, str): |
| #244 | user_message = msg |
| #245 | break |
| #246 | |
| #247 | if user_message and len(user_message) > 3: |
| #248 | # Search across ALL sessions (no session filter) for semantic matches |
| #249 | recall_results = mem.recall( |
| #250 | query=user_message, |
| #251 | top_k=5, |
| #252 | temporal_weight=0.2 # mild recency bias |
| #253 | ) |
| #254 | |
| #255 | # Deduplicate: remove recall results that are already in context_memories |
| #256 | context_ids = {m.get("id") for m in context_memories} |
| #257 | unique_recall = [r for r in recall_results if r.get("id") not in context_ids] |
| #258 | |
| #259 | if not context_memories and not unique_recall: |
| #260 | return None # No context to inject |
| #261 | |
| #262 | # Build context block |
| #263 | lines = ["═══════════════════════════════════════════════════════════════"] |
| #264 | |
| #265 | if context_memories: |
| #266 | lines.append("MNEMOSYNE CONTEXT (recent, current session)") |
| #267 | lines.append("") |
| #268 | for m in context_memories: |
| #269 | imp = m.get('importance', 0) |
| #270 | raw = m['content'][:300] if len(m['content']) > 300 else m['content'] |
| #271 | content = _compress_memory(raw) |
| #272 | ts = m['timestamp'][:16] if len(m['timestamp']) > 16 else m['timestamp'] |
| #273 | scope = m.get('scope', 'session') |
| #274 | lines.append(f"[{ts}] imp={imp:.1f} scope={scope} {content}") |
| #275 | lines.append("") |
| #276 | |
| #277 | if unique_recall: |
| #278 | lines.append("MNEMOSYNE RECALL (semantic search, all sessions)") |
| #279 | lines.append("") |
| #280 | for r in unique_recall: |
| #281 | imp = r.get('importance', 0) |
| #282 | raw = r['content'][:300] if len(r['content']) > 300 else r['content'] |
| #283 | content = _compress_memory(raw) |
| #284 | ts = r.get('timestamp', '')[:16] |
| #285 | score = r.get('score', 0) |
| #286 | scope = r.get('scope', 'session') |
| #287 | lines.append(f"[{ts}] imp={imp:.1f} score={score:.2f} scope={scope} {content}") |
| #288 | lines.append("") |
| #289 | |
| #290 | lines.append("═══════════════════════════════════════════════════════════════") |
| #291 | context_block = "\n".join(lines) |
| #292 | full_context = f"\n\n{context_block}\n" |
| #293 | |
| #294 | return {"context": full_context} |
| #295 | |
| #296 | except Exception as e: |
| #297 | import logging |
| #298 | logging.getLogger(__name__).warning( |
| #299 | "Mnemosyne _on_pre_llm_call hook failed (session=%s): %s", |
| #300 | session_id, e |
| #301 | ) |
| #302 | return None |
| #303 | |
| #304 | |
| #305 | def _on_post_tool_call(tool_name, args, result, **kwargs): |
| #306 | """ |
| #307 | Hook for post-tool-call processing. |
| #308 | |
| #309 | Auto-logging of tool calls is disabled by default because it quickly |
| #310 | floods working_memory with low-signal operational noise (every terminal |
| #311 | command, file write, etc.). Users can opt-in via MNEMOSYNE_LOG_TOOLS=1. |
| #312 | |
| #313 | If you want to remember the outcome of a tool call, use mnemosyne_remember |
| #314 | explicitly from the conversation instead. |
| #315 | """ |
| #316 | try: |
| #317 | if not os.environ.get("MNEMOSYNE_LOG_TOOLS"): |
| #318 | return |
| #319 | |
| #320 | mem = _get_memory() |
| #321 | |
| #322 | # Only log if explicitly opted in; keep importance low so these |
| #323 | # don't pollute prompt context injection. |
| #324 | if tool_name in ['terminal', 'execute_code', 'write_file', 'patch']: |
| #325 | summary = f"Tool {tool_name} executed" |
| #326 | if args: |
| #327 | summary += f" with args: {str(args)[:100]}" |
| #328 | |
| #329 | mem.remember( |
| #330 | content=f"[TOOL] {summary}", |
| #331 | source="tool_execution", |
| #332 | importance=0.1 |
| #333 | ) |
| #334 | except: |
| #335 | pass # Fail silently |
| #336 |