diff --git a/navi/core/registry.py b/navi/core/registry.py index 4f80fcd..dc22862 100644 --- a/navi/core/registry.py +++ b/navi/core/registry.py @@ -15,6 +15,7 @@ MemorySearchTool, SpawnAgentTool, SshExecTool, + SwitchProfileTool, TerminalTool, TodoTool, Tool, @@ -125,8 +126,7 @@ for p in ALL_PROFILES: profiles.register(p) - # SpawnAgentTool registered after profiles so it can resolve profile names. - # session_store may be None at build time (injected separately in deps.py). + # Tools that need session_store + profile_registry registered after both are built. spawn_tool = SpawnAgentTool( profile_registry=profiles, tool_registry=tools, @@ -136,6 +136,12 @@ ) tools.register(spawn_tool, builtin=True) + switch_tool = SwitchProfileTool( + session_store=session_store, + profile_registry=profiles, + ) + tools.register(switch_tool, builtin=True) + backends = BackendRegistry() backends.register( "ollama", diff --git a/navi/profiles/secretary.py b/navi/profiles/secretary.py index 4788ab5..d3c809a 100644 --- a/navi/profiles/secretary.py +++ b/navi/profiles/secretary.py @@ -17,7 +17,7 @@ For complex multi-part tasks (3+ tool calls): call todo(op="set", tasks=[...]) first, then execute step by step. Mark each step done/failed with todo(op="update") as you go. Output style: concise, structured. When researching, include sources. Match tone and format to what was asked.""", - enabled_tools=["todo", "web_search", "web_view", "http_request", "filesystem", "code_exec", "terminal", "image_view", "memory_search", "memory_forget", "reload_tools", "write_tool", "list_tools", "tool_manual", "spawn_agent"], + enabled_tools=["todo", "switch_profile", "web_search", "web_view", "http_request", "filesystem", "code_exec", "terminal", "image_view", "memory_search", "memory_forget", "reload_tools", "write_tool", "list_tools", "tool_manual", "spawn_agent"], model="gemma4:26b-a4b-it-q4_K_M", temperature=0.7, max_iterations=30, diff --git a/navi/profiles/server_admin.py b/navi/profiles/server_admin.py index 3565d38..b970e8a 100644 --- a/navi/profiles/server_admin.py +++ b/navi/profiles/server_admin.py @@ -21,7 +21,7 @@ 3. Before destructive or irreversible operations, state what you're about to do and why. When delegating to sub-agents: assign each a single host or a single domain of concern. Include exact connection details and expected output format in every briefing.""", - enabled_tools=["todo", "terminal", "filesystem", "http_request", "web_view", "web_search", "ssh_exec", "image_view", "memory_search", "memory_forget", "reload_tools", "write_tool", "list_tools", "tool_manual", "spawn_agent"], + enabled_tools=["todo", "switch_profile", "terminal", "filesystem", "http_request", "web_view", "web_search", "ssh_exec", "image_view", "memory_search", "memory_forget", "reload_tools", "write_tool", "list_tools", "tool_manual", "spawn_agent"], model="gemma4:26b-a4b-it-q4_K_M", temperature=0.2, max_iterations=30, diff --git a/navi/profiles/smart_home.py b/navi/profiles/smart_home.py index d9be7d4..115dae9 100644 --- a/navi/profiles/smart_home.py +++ b/navi/profiles/smart_home.py @@ -19,7 +19,7 @@ 1. For multi-step tasks (3+ tool calls): call todo(op="set", tasks=[...]) first — which entities, what to check or change, in what order. Mark each step with todo(op="update") as you go. 2. Before writing any HA config to disk, validate structure in code_exec. 3. Before toggling devices or triggering automations, state what will change and whether it's reversible.""", - enabled_tools=["todo", "http_request", "web_view", "filesystem", "code_exec", "terminal", "ssh_exec", "image_view", "memory_search", "memory_forget", "reload_tools", "write_tool", "list_tools", "tool_manual", "spawn_agent"], + enabled_tools=["todo", "switch_profile", "http_request", "web_view", "filesystem", "code_exec", "terminal", "ssh_exec", "image_view", "memory_search", "memory_forget", "reload_tools", "write_tool", "list_tools", "tool_manual", "spawn_agent"], model="gemma4:26b-a4b-it-q4_K_M", temperature=0.3, max_iterations=30, diff --git a/navi/tools/__init__.py b/navi/tools/__init__.py index b331b0d..6247474 100644 --- a/navi/tools/__init__.py +++ b/navi/tools/__init__.py @@ -9,6 +9,7 @@ from .memory_forget import MemoryForgetTool from .memory_search import MemorySearchTool from .todo import TodoTool +from .switch_profile import SwitchProfileTool from .web_search import WebSearchTool from .web_view import WebViewTool @@ -27,4 +28,5 @@ "MemoryForgetTool", "SpawnAgentTool", "TodoTool", + "SwitchProfileTool", ] diff --git a/navi/tools/switch_profile.py b/navi/tools/switch_profile.py new file mode 100644 index 0000000..6add542 --- /dev/null +++ b/navi/tools/switch_profile.py @@ -0,0 +1,65 @@ +"""Built-in tool for switching the active agent profile mid-session.""" + +from navi.tools.base import Tool, ToolResult, current_session_id + + +class SwitchProfileTool(Tool): + name = "switch_profile" + description = ( + "Switch this session to a different agent profile. " + "Use when the task domain changes and another profile has better-suited tools or instructions. " + "The new profile (tools + system prompt) becomes active from the NEXT user message. " + "Always tell the user you switched and which profile is now active." + ) + parameters = { + "type": "object", + "properties": { + "profile_id": { + "type": "string", + "description": "ID of the profile to switch to.", + } + }, + "required": ["profile_id"], + } + + def __init__(self, session_store, profile_registry) -> None: + self._sessions = session_store + self._profiles = profile_registry + + async def execute(self, params: dict) -> ToolResult: + profile_id = (params.get("profile_id") or "").strip() + available = ", ".join(p.id for p in self._profiles.all()) + + try: + profile = self._profiles.get(profile_id) + except Exception: + return ToolResult( + success=False, + output="", + error=f"Profile '{profile_id}' not found. Available: {available}", + ) + + sid = current_session_id.get() + if not sid: + return ToolResult(success=False, output="", error="No active session context.") + + session = await self._sessions.get(sid) + if session is None: + return ToolResult(success=False, output="", error="Session not found.") + + if session.profile_id == profile_id: + return ToolResult( + success=True, + output=f"Already on profile '{profile.name}' — no change.", + ) + + session.profile_id = profile_id + await self._sessions.save(session) + + return ToolResult( + success=True, + output=( + f"Switched to profile '{profile.name}' ({profile_id}). " + f"Its tools and system prompt are active from the next message." + ), + ) diff --git a/persona.txt b/persona.txt index 4516712..6a4c74e 100644 --- a/persona.txt +++ b/persona.txt @@ -22,6 +22,16 @@ write_tool reports success or the exact error. If there is an error, fix the code and call write_tool again. The tool is available from the NEXT user message. To enable it in a profile, add the name to enabled_tools in navi/profiles/.py. +PROFILE SWITCHING: +Each session has an active profile — it defines your available tools and system instructions. When the user's task clearly belongs to a different domain (e.g. switching from writing to server administration, or from admin work to home automation), call switch_profile with the appropriate profile_id. + +Rules: +- Call list_tools or check the profiles available if unsure which profile_id to use. +- The switch takes effect from the NEXT user message — your current tools are still available for this turn. +- After switching, tell the user: which profile is now active and what it's for. +- Don't switch for a single off-topic question. Switch when the session is clearly moving into a different domain. +- Never switch back and forth repeatedly within one conversation. + WORKSPACE: You have a persistent workspace directory at workspace/ (relative to the project root). Use it freely for any long-term files: scripts, notes, data, configs, research results — anything worth keeping across sessions. It is yours; the user will not clean it up. Do NOT write working files to the project root.