diff --git a/navi/core/registry.py b/navi/core/registry.py index fee1fed..a768eee 100644 --- a/navi/core/registry.py +++ b/navi/core/registry.py @@ -14,6 +14,7 @@ ImageViewTool, ListProfilesTool, MemoryTool, + ReflectTool, SpawnAgentTool, SshExecTool, ScratchpadTool, @@ -127,7 +128,8 @@ builtins = [WebSearchTool(), FilesystemTool(ai_helper=ai_helper), HttpRequestTool(), WebViewTool(), CodeExecTool(), TerminalTool(), SshExecTool(), ImageViewTool(), ShareFileTool(), TestToolTool(), - TodoTool(), ScratchpadTool(), reload_tool, write_tool, delete_tool, list_tool, manual_tool] + TodoTool(), ScratchpadTool(), ReflectTool(ai_helper=ai_helper), + reload_tool, write_tool, delete_tool, list_tool, manual_tool] if memory_tool: builtins.append(memory_tool) for builtin in builtins: diff --git a/navi/profiles/developer/config.json b/navi/profiles/developer/config.json index c45baa8..4aa8ed6 100644 --- a/navi/profiles/developer/config.json +++ b/navi/profiles/developer/config.json @@ -14,7 +14,7 @@ "max_iterations": 40, "planning_enabled": true, "enabled_tools": [ - "todo", "scratchpad", "switch_profile", "list_profiles", + "todo", "scratchpad", "reflect", "switch_profile", "list_profiles", "web_search", "web_view", "http_request", "filesystem", "code_exec", "terminal", "image_view", "memory", diff --git a/navi/profiles/secretary/config.json b/navi/profiles/secretary/config.json index be6ed05..ed3b606 100644 --- a/navi/profiles/secretary/config.json +++ b/navi/profiles/secretary/config.json @@ -14,7 +14,7 @@ "max_iterations": 40, "planning_enabled": true, "enabled_tools": [ - "todo", "scratchpad", "switch_profile", "list_profiles", + "todo", "scratchpad", "reflect", "switch_profile", "list_profiles", "web_search", "web_view", "http_request", "filesystem", "code_exec", "image_view", "memory", diff --git a/navi/profiles/server_admin/config.json b/navi/profiles/server_admin/config.json index fa47a59..2e38fe4 100644 --- a/navi/profiles/server_admin/config.json +++ b/navi/profiles/server_admin/config.json @@ -14,7 +14,7 @@ "max_iterations": 40, "planning_enabled": true, "enabled_tools": [ - "todo", "scratchpad", "switch_profile", "list_profiles", + "todo", "scratchpad", "reflect", "switch_profile", "list_profiles", "web_search", "web_view", "http_request", "filesystem", "code_exec", "terminal", "ssh_exec", "image_view", "memory", diff --git a/navi/tools/__init__.py b/navi/tools/__init__.py index d6a0a8f..3296243 100644 --- a/navi/tools/__init__.py +++ b/navi/tools/__init__.py @@ -13,6 +13,7 @@ from .scratchpad import ScratchpadTool from .switch_profile import SwitchProfileTool from .list_profiles import ListProfilesTool +from .reflect import ReflectTool from .web_search import WebSearchTool from .web_view import WebViewTool @@ -35,4 +36,5 @@ "ScratchpadTool", "SwitchProfileTool", "ListProfilesTool", + "ReflectTool", ] diff --git a/navi/tools/reflect.py b/navi/tools/reflect.py new file mode 100644 index 0000000..e5b283d --- /dev/null +++ b/navi/tools/reflect.py @@ -0,0 +1,157 @@ +""" +reflect — structured multi-perspective thinking tool. + +Runs three parallel AIHelper calls with distinct advisor roles +(Critic, Pragmatist, Detailer) and returns their independent perspectives. +Use before planning a complex task or when stuck on a problem. + +Designed to force explicit articulation of assumptions — the most common +source of planning errors — and get fresh perspectives unclouded by +accumulated conversation context. +""" + +import asyncio + +from navi.tools.base import Tool, ToolResult + +# ── Advisor system prompts ───────────────────────────────────────────────── + +_CRITIC_SYSTEM = """\ +You are a critical advisor. Your role is to challenge the situation description and expose weaknesses. + +Focus on: +- Which stated assumptions are likely wrong or unverified? +- What risks and failure modes are being ignored? +- What logical gaps or contradictions exist in the approach? +- What is being taken for granted that should be questioned? + +Be direct and specific. Do not be encouraging. Name concrete problems, not abstract concerns. +Keep your response concise — 3 to 6 sharp points.""" + +_PRAGMATIST_SYSTEM = """\ +You are a pragmatic advisor. Your role is to find the simplest, most direct path to the goal. + +Focus on: +- What is the minimal viable approach that actually solves the problem? +- What complexity can be eliminated without losing the core outcome? +- What alternatives or shortcuts have not been considered? +- Is the stated goal actually the real goal, or is there a simpler reframing? + +Challenge over-engineering. Propose concrete simplifications. +Keep your response concise — 3 to 6 actionable points.""" + +_DETAILER_SYSTEM = """\ +You are a detail-oriented advisor. Your role is to find what is missing, ambiguous, or underspecified. + +Focus on: +- What edge cases or failure scenarios have not been addressed? +- What requirements or constraints are implied but not stated? +- What implementation details will become blockers later if not resolved now? +- What information is missing before a good decision can be made? + +Be specific about what is absent, not just that something might be missing. +Keep your response concise — 3 to 6 concrete gaps.""" + +# ── Prompt builder ───────────────────────────────────────────────────────── + +def _build_user_prompt(situation: str, assumptions: list[str], tried: str | None) -> str: + parts = [f"## Situation\n{situation.strip()}"] + + if assumptions: + bullet_assumptions = "\n".join(f"- {a.strip()}" for a in assumptions if a.strip()) + parts.append(f"## Assumptions being made\n{bullet_assumptions}") + else: + parts.append("## Assumptions being made\n(none stated)") + + if tried and tried.strip(): + parts.append(f"## Already attempted\n{tried.strip()}") + + return "\n\n".join(parts) + + +# ── Tool ─────────────────────────────────────────────────────────────────── + +class ReflectTool(Tool): + name = "reflect" + description = ( + "Get three independent expert perspectives on a situation before planning or when stuck.\n\n" + "Call this when:\n" + "- About to plan a complex or ambiguous task\n" + "- Stuck on a problem and need a fresh angle\n" + "- Unsure whether your approach is right\n\n" + "Three advisors analyse your situation in parallel:\n" + "· Critic — challenges assumptions, surfaces risks and flaws\n" + "· Pragmatist — finds the simplest path, cuts unnecessary complexity\n" + "· Detailer — spots missing requirements, edge cases, and gaps\n\n" + "IMPORTANT: The `assumptions` field is mandatory and is the most valuable input. " + "List every belief you are acting on without having verified it. " + "The act of listing assumptions often reveals the problem itself." + ) + parameters = { + "type": "object", + "properties": { + "situation": { + "type": "string", + "description": ( + "Describe the goal and the current situation clearly. " + "Include: what you are trying to achieve, the approach you are considering, " + "and what specifically you are unsure about." + ), + }, + "assumptions": { + "type": "array", + "items": {"type": "string"}, + "description": ( + "List every assumption you are making — things you believe are true " + "but have not verified. Be honest and thorough. " + "Example: 'the API returns data in this format', " + "'the user wants X not Y', 'this file will always exist'." + ), + }, + "tried": { + "type": "string", + "description": ( + "Optional. What you have already attempted and why it did not work. " + "Provide this when you are stuck, not when planning from scratch." + ), + }, + }, + "required": ["situation", "assumptions"], + } + + def __init__(self, ai_helper) -> None: + self._ai = ai_helper + + async def execute(self, params: dict) -> ToolResult: + situation = (params.get("situation") or "").strip() + assumptions = params.get("assumptions") or [] + tried = (params.get("tried") or "").strip() or None + + if not situation: + return ToolResult(success=False, output="", error="situation is required") + + user_prompt = _build_user_prompt(situation, assumptions, tried) + + # Run all three advisors in parallel + critic_task = self._ai.ask(_CRITIC_SYSTEM, user_prompt) + pragmatist_task = self._ai.ask(_PRAGMATIST_SYSTEM, user_prompt) + detailer_task = self._ai.ask(_DETAILER_SYSTEM, user_prompt) + + critic, pragmatist, detailer = await asyncio.gather( + critic_task, pragmatist_task, detailer_task + ) + + output = ( + "# Reflection\n\n" + "## 🔴 Critic\n" + f"{critic}\n\n" + "## 🟡 Pragmatist\n" + f"{pragmatist}\n\n" + "## 🔵 Detailer\n" + f"{detailer}\n\n" + "---\n" + "Integrate these perspectives into your plan. " + "Prioritise addressing points raised by the Critic before proceeding." + ) + + return ToolResult(success=True, output=output) diff --git a/persona.txt b/persona.txt index 2ac1a91..445a9c1 100644 --- a/persona.txt +++ b/persona.txt @@ -32,6 +32,18 @@ - Difficulties, errors, failed commands, and sub-agent failures are NOT fundamental blockers — they are problems to solve. - When the task is complete or fundamentally blocked, report the outcome once, concisely. +REFLECTION: +You have a reflect tool that gives you three independent expert perspectives on any situation: a Critic (challenges assumptions and risks), a Pragmatist (finds the simplest path), and a Detailer (spots missing requirements and edge cases). All three run in parallel — it is fast. + +Use reflect when: +- About to plan a complex or ambiguous task (call it BEFORE setting your todo list). +- Stuck on a problem and your current approach is not working. +- Unsure whether your reading of the user's request is correct. + +The `assumptions` parameter is the most important input. Be honest: list every belief you are acting on without having verified it. This alone often reveals the source of confusion. + +Do NOT use reflect for simple, clearly-scoped tasks. It is for situations with genuine ambiguity, complexity, or risk. + RESPONSE HYGIENE: Never include internal tracking state in your final response: - Plan progress lines ("Plan — N/M done:", todo status lists).