diff --git a/navi/core/registry.py b/navi/core/registry.py index e9df3fd..db2b53f 100644 --- a/navi/core/registry.py +++ b/navi/core/registry.py @@ -12,6 +12,7 @@ HttpRequestTool, ImageViewTool, MemoryForgetTool, + MemorySaveTool, MemorySearchTool, SpawnAgentTool, SshExecTool, @@ -111,12 +112,13 @@ manual_tool = ToolManualTool(registry=tools) memory_search = MemorySearchTool(memory_store) if memory_store else None memory_forget = MemoryForgetTool(memory_store) if memory_store else None + memory_save = MemorySaveTool(memory_store) if memory_store else None builtins = [WebSearchTool(), FilesystemTool(), HttpRequestTool(), WebViewTool(), CodeExecTool(), TerminalTool(), SshExecTool(), ImageViewTool(), ShareFileTool(), TodoTool(), ScratchpadTool(), reload_tool, write_tool, list_tool, manual_tool] if memory_search: - builtins.extend([memory_search, memory_forget]) + builtins.extend([memory_search, memory_forget, memory_save]) for builtin in builtins: tools.register(builtin, builtin=True) diff --git a/navi/profiles/secretary.py b/navi/profiles/secretary.py index 16af71f..b84436b 100644 --- a/navi/profiles/secretary.py +++ b/navi/profiles/secretary.py @@ -30,7 +30,7 @@ "todo", "scratchpad", "switch_profile", "web_search", "web_view", "http_request", "filesystem", "code_exec", "image_view", - "memory_search", "memory_forget", + "memory_search", "memory_forget", "memory_save", "list_tools", "tool_manual", "spawn_agent", "share_file", diff --git a/navi/profiles/server_admin.py b/navi/profiles/server_admin.py index c788941..7c3cbe4 100644 --- a/navi/profiles/server_admin.py +++ b/navi/profiles/server_admin.py @@ -40,7 +40,7 @@ "todo", "scratchpad", "switch_profile", "web_search", "web_view", "http_request", "filesystem", "code_exec", "terminal", "ssh_exec", "image_view", - "memory_search", "memory_forget", + "memory_search", "memory_forget", "memory_save", "reload_tools", "write_tool", "list_tools", "tool_manual", "spawn_agent", "share_file", diff --git a/navi/profiles/smart_home.py b/navi/profiles/smart_home.py index 5edcd93..dbf03a3 100644 --- a/navi/profiles/smart_home.py +++ b/navi/profiles/smart_home.py @@ -34,7 +34,7 @@ "todo", "scratchpad", "switch_profile", "web_search", "web_view", "http_request", "filesystem", "code_exec", "ssh_exec", "image_view", - "memory_search", "memory_forget", + "memory_search", "memory_forget", "memory_save", "reload_tools", "write_tool", "list_tools", "tool_manual", "spawn_agent", "share_file", diff --git a/navi/tools/__init__.py b/navi/tools/__init__.py index 1c07ae0..839eaf3 100644 --- a/navi/tools/__init__.py +++ b/navi/tools/__init__.py @@ -7,6 +7,7 @@ from .spawn_agent import SpawnAgentTool from .terminal import TerminalTool from .memory_forget import MemoryForgetTool +from .memory_save import MemorySaveTool from .memory_search import MemorySearchTool from .todo import TodoTool from .scratchpad import ScratchpadTool @@ -27,6 +28,7 @@ "WebViewTool", "MemorySearchTool", "MemoryForgetTool", + "MemorySaveTool", "SpawnAgentTool", "TodoTool", "ScratchpadTool", diff --git a/navi/tools/memory_save.py b/navi/tools/memory_save.py new file mode 100644 index 0000000..2eed1f2 --- /dev/null +++ b/navi/tools/memory_save.py @@ -0,0 +1,71 @@ +"""Memory save tool — persist a fact about the user to long-term memory.""" + +from navi.memory.store import MemoryStore +from navi.tools.base import current_session_id + +from .base import Tool, ToolResult + +_VALID_CATEGORIES = {"profile", "preferences", "technical", "projects", "other"} + + +class MemorySaveTool(Tool): + name = "memory_save" + description = ( + "Save or update a fact about the user in long-term memory. " + "Use this when the user tells you something stable and reusable — name, location, " + "preferences, ongoing projects, technical environment, recurring workflows, etc. " + "Also use it to correct an outdated fact you know is wrong. " + "Facts survive across sessions and are available via memory_search." + ) + parameters = { + "type": "object", + "properties": { + "category": { + "type": "string", + "enum": ["profile", "preferences", "technical", "projects", "other"], + "description": ( + "Broad category for the fact. " + "profile=who they are, preferences=what they like/dislike, " + "technical=OS/tools/servers/languages, projects=ongoing work, other=anything else." + ), + }, + "key": { + "type": "string", + "description": ( + "Short snake_case identifier, unique within the category. " + "Examples: 'name', 'primary_os', 'home_server_ip', 'preferred_language', " + "'current_project', 'response_language'." + ), + }, + "value": { + "type": "string", + "description": "The fact itself, written as a concise plain-text statement.", + }, + }, + "required": ["category", "key", "value"], + } + + def __init__(self, memory_store: MemoryStore) -> None: + self._store = memory_store + + async def execute(self, params: dict) -> ToolResult: + category = params.get("category", "").strip().lower() + key = params.get("key", "").strip() + value = params.get("value", "").strip() + + if not category: + return ToolResult(success=False, output="category is required.", error="missing category") + if category not in _VALID_CATEGORIES: + return ToolResult( + success=False, + output=f"Invalid category '{category}'. Must be one of: {', '.join(sorted(_VALID_CATEGORIES))}", + error="invalid category", + ) + if not key: + return ToolResult(success=False, output="key is required.", error="missing key") + if not value: + return ToolResult(success=False, output="value is required.", error="missing value") + + session_id = current_session_id.get(None) + await self._store.upsert_fact(category, key, value, session_id) + return ToolResult(success=True, output=f"Saved [{category}] {key}: {value}") diff --git a/persona.txt b/persona.txt index 3327d00..1e8ceda 100644 --- a/persona.txt +++ b/persona.txt @@ -122,7 +122,14 @@ Do NOT call memory_search as a reflex at the start of every session — only when the context genuinely calls for it. -Call memory_forget only when the user explicitly asks you to forget something, or when you know a stored fact is clearly wrong or outdated. +Call memory_save immediately when: +- The user tells you anything stable and reusable: their name, location, employer, family, devices, server IPs, recurring projects, language preference, habits, or explicit instructions about how they want to be helped. +- You notice an existing fact is wrong or outdated — correct it with a fresh memory_save call (no need to memory_forget first; save overwrites by key). +- The user says "remember that..." or similar. + +Do NOT save ephemeral facts: temporary states, one-off tasks, or things specific to this session only. + +Call memory_forget only when the user explicitly asks you to forget something that memory_save cannot replace (e.g., "delete everything about X"), or when a key no longer exists in any useful form. DOCUMENTATION: Project docs live in docs/ (architecture, agent loop, tools, API reference, profiles, etc.).