Newer
Older
navi-1 / CLAUDE.md

Navi — Project Context for Claude

What this is

Personal modular AI agent system. FastAPI backend + vanilla JS client. The agent is named Navi — female, loyal, uncensored personal assistant. Runs locally via Ollama.

Stack

  • Backend: FastAPI, uvicorn, aiosqlite, structlog, pydantic-settings
  • LLM: Ollama (primary), OpenAI-compatible backend also wired
  • Current model: gemma4:e2b-it-q8_0 (2B active params, Q8 — small but capable)
  • Thinking: ollama_think: bool = True — model reasoning is enabled and streamed to client
  • Client: Vanilla JS ES modules, marked.js + highlight.js via esm.sh CDN
  • DB: SQLite via aiosqlite for persistent sessions
  • Run: .venv/bin/uvicorn navi.main:app --reload --reload-dir navi --port 8000

Key architecture

Agent loop (navi/core/agent.py)

Tool-calling loop with llm.complete() for tool turns, llm.stream() for final response. Events yielded: ToolEvent, ThinkingDelta, ThinkingEnd, TextDelta, StreamEnd. Tool schemas built fresh on every run_stream() call from registry + tools/enabled.json.

Tool system

Two tiers:

Built-in tools (navi/tools/):

  • web_search, filesystem, http_request, code_exec, terminal, ssh_exec, image_view
  • write_tool — Navi's primary self-extension mechanism (writes + reloads in one call)
  • reload_tools — hot-reload all user tools without server restart
  • list_tools — returns actual live tool list from registry (Navi calls this when asked what she can do)
  • tool_manual — returns manuals/<tool_name>.md if exists, else auto-generates from schema

User tools (tools/*.py):

  • Written by Navi via write_tool, or manually
  • Module-level format: name, description, parameters, async def execute(params) -> str
  • No classes, no module-level print(), execute must return plain string or raise
  • Auto-discovered at startup and on reload
  • tools/enabled.json — list of user tool names to auto-include in all profiles
  • tools/_template.py — canonical format reference (starts with _, not auto-loaded)

Currently created by Navi: get_current_datetime.py, user_notes.py (working, correct format).

Profiles (navi/profiles/)

secretary, server_admin, smart_home. Each has enabled_tools, system_prompt, model, temperature, max_iterations. All profiles have the same built-in tool set: [..., reload_tools, write_tool, list_tools, tool_manual]. User tools from enabled.json are merged in by Agent._tool_list().

Global persona (navi/config.py.env)

NAVI_PERSONA env var — prepended to every profile's system prompt separated by ---. Contains: personality, self-extension instructions, write_tool usage rules, tool_manual usage.

Registry (navi/core/registry.py)

ToolRegistry tracks _builtin_names to distinguish builtins from user tools on reload. reload_user_tools() drops all non-builtins and reloads from disk. Built-in tools with registry injection: ReloadToolsTool, WriteToolTool, ListToolsTool, ToolManualTool.

Sessions (navi/core/sqlite_session_store.py)

Persistent SQLite sessions. model_dump(mode='json') required for datetime serialization. Session ID in URL hash for bookmarking.

WebSocket protocol (navi/api/websocket.py)

client → server: {type: "message", content: "...", images: [...]}
server → client: stream_start
                 thinking_delta {delta}   ← reasoning chunks (collapsible in UI)
                 thinking_end
                 tool_call {tool, args, result, success}
                 stream_delta {delta}
                 stream_end {content}
                 error {message}

Client (client/)

ES modules: app.js (state/routing), chat.js (DOM helpers), ws.js (WebSocket), api.js (REST), sidebar.js. Thinking blocks: open during reasoning, auto-collapse on thinking_end, re-openable (like tool cards). Tool cards: accordion, collapsed by default, click to expand. Images: paste/attach, base64 via FileReader, rendered in bubbles. No localStorage — session from URL hash or most recent server session.

Dynamic tool loading (navi/tools/loader.py)

Tries module-level format first (preferred for user tools), falls back to class-based. Errors isolated per file — one broken file doesn't affect others. Detailed error messages: lists exactly which required definitions are missing.

Config (.env)

NAVI_PERSONA="..."          # global personality + tool writing rules
OLLAMA_HOST=...
OLLAMA_DEFAULT_MODEL=gemma4:e2b-it-q8_0
OLLAMA_NUM_CTX=8192
OLLAMA_THINK=true

Manuals (manuals/)

Markdown files, one per tool. tool_manual serves them on demand. Currently: manuals/write_tool.md (full format reference + working example). Auto-generation fallback from tool schema if no .md exists.

Important patterns

  • write_tool validates code before writing (checks for 4 required definitions)
  • write_tool adds tool to tools/enabled.json on success → available in all profiles
  • New tool available from the next user message (tool schemas built at run_stream() entry)
  • Navi should call tool_manual("write_tool") before writing a tool
  • Navi should call list_tools when asked about her capabilities (not generate from memory)
  • no-store cache middleware on /static/ — safe to hard-refresh during development

What works well

  • Hot-reload without server restart
  • Thinking display in client
  • Self-extension via write_tool (improving — model still sometimes struggles with format)
  • Session persistence, URL-based navigation

Known friction

  • Small model (e2b) sometimes writes tools in wrong format despite detailed instructions
  • tool_manual + explicit format feedback in write_tool errors is the current mitigation
  • Navi tends to hallucinate tool lists — list_tools fixes this if she uses it