"""Tools that let the agent improve itself between conversations. Three kinds of thing get learned in a voice conversation, and they belong in different places: - **How a word was misheard** is about this microphone and this recogniser. It goes to `corrections.txt` in the workspace — local, because it would be meaningless on another machine. - **A lasting preference** — "stop explaining so much" — goes to the personal brain's `preferences.md`, because it is true of Adolfo regardless of which assistant is listening. - **A technique or lesson** goes to the brain's `notes.md`, same reasoning. This module is only the mechanism. *When* to record something, and which file it belongs in, is personal setup rather than a property of the agent, so that lives in a skill at `~/Workspace/.claude/skills/memory/`. Editing the discipline means editing that file, not this one. Everything written lands in a git-tracked file or the brain, so it can be reviewed and undone. """ from pathlib import Path from claude_agent_sdk import create_sdk_mcp_server, tool from loguru import logger SERVER_NAME = "memory" # The tool names Claude sees, and must be allowed to call. TOOL_NAMES = [ f"mcp__{SERVER_NAME}__remember_correction", f"mcp__{SERVER_NAME}__remember_preference", f"mcp__{SERVER_NAME}__remember_note", ] def build_server(*, workspace: Path, brain=None): """Create the in-process MCP server exposing the memory tools.""" corrections_file = workspace / "corrections.txt" @tool( "remember_correction", "Record that speech recognition misheard a word, so it is fixed from now on.", {"heard": str, "intended": str}, ) async def remember_correction(args): heard = (args.get("heard") or "").strip() intended = (args.get("intended") or "").strip() if not heard or not intended or heard.lower() == intended.lower(): return {"content": [{"type": "text", "text": "Nothing to record."}]} # A one-word rule risks firing on ordinary speech; a phrase is safer. rule = f"{heard} => {intended}" existing = corrections_file.read_text() if corrections_file.exists() else "" if rule.lower() in existing.lower(): return {"content": [{"type": "text", "text": "Already known."}]} with corrections_file.open("a") as f: if not existing.endswith("\n"): f.write("\n") f.write(f"{rule}\n") logger.info(f"Learned correction: {rule}") return {"content": [{"type": "text", "text": f"Recorded: {rule}"}]} @tool( "remember_preference", "Record a lasting preference about how Adolfo wants to be worked with.", {"preference": str}, ) async def remember_preference(args): return _to_brain(brain, "preference", args.get("preference")) @tool( "remember_note", "Record a technique, lesson or fact worth having in future sessions.", {"note": str}, ) async def remember_note(args): return _to_brain(brain, "note", args.get("note")) return create_sdk_mcp_server( name=SERVER_NAME, tools=[remember_correction, remember_preference, remember_note], ) def _to_brain(brain, kind: str, text: str | None) -> dict: text = (text or "").strip() if not text: return {"content": [{"type": "text", "text": "Nothing to record."}]} if brain is None or not brain.reachable: logger.warning(f"Brain unreachable; dropped {kind}: {text[:60]}") return {"content": [{"type": "text", "text": "Memory unavailable right now."}]} ok, detail = brain.append(kind, f"- {text}") message = f"Recorded to {detail}." if ok else f"Could not record: {detail}" return {"content": [{"type": "text", "text": message}]}