Newer
Older
navi-1 / docs / architecture.md

Architecture

High-level component map and data flow for the Navi backend.

Component diagram

┌─────────────────────────────────────────────────────────┐
│  Client (browser)                                       │
│  WebSocket /ws/sessions/{id}    REST /sessions/*        │
└──────────────┬──────────────────────────┬───────────────┘
               │ WS frames               │ HTTP
               ▼                         ▼
┌─────────────────────────────────────────────────────────┐
│  FastAPI (navi/main.py)                                 │
│  ┌──────────────────┐  ┌───────────────────────────┐   │
│  │  websocket.py    │  │  routes/sessions.py        │   │
│  │  orchestrator    │  │  routes/messages.py        │   │
│  │  stop endpoint   │  │  routes/agents.py          │   │
│  └────────┬─────────┘  └────────────┬──────────────┘   │
└───────────┼─────────────────────────┼───────────────────┘
            │                         │
            ▼                         ▼
┌──────────────────────────────────────────────────────────┐
│  Agent (navi/core/agent.py)                              │
│  run_stream() → AsyncGenerator[AgentEvent]               │
│  run()        → str  (non-streaming)                     │
│  run_ephemeral() → str  (subagent, no DB)                │
│                                                          │
│  ┌──────────────┐  ┌───────────────┐  ┌──────────────┐  │
│  │ Planning     │  │ Tool-calling  │  │  Workers     │  │
│  │ _run_planning│  │ loop          │  │ (compression)│  │
│  └──────────────┘  └───────────────┘  └──────────────┘  │
└───┬──────────────┬────────────────┬─────────────────────┘
    │              │                │
    ▼              ▼                ▼
┌────────┐  ┌──────────────┐  ┌────────────────────────┐
│ LLM    │  │  ToolRegistry│  │  SessionStore          │
│Backend │  │  (built-ins  │  │  (PostgreSQL)  │
│(Ollama)│  │  + user tools│  │                        │
└────────┘  └──────────────┘  └────────────────────────┘
                                        │
                              ┌─────────┴──────────┐
                              │   MemoryStore       │
                              │   (PostgreSQL + pgvector)    │
                              └────────────────────┘

Request lifecycle (streaming)

  1. Client sends {type: "message", content: "..."} over WebSocket.
  2. AgentSessionOrchestrator creates a SessionRun, subscribes a queue, and launches the agent task.
  3. The agent task calls agent.run_stream(session_id, content).
  4. run_stream(): a. Loads session + profile from store. b. Pre-turn: checks if context needs compression; compresses if threshold exceeded. c. Planning phase (if profile.planning_enabled): calls LLM once (non-streaming, no tools) to produce a step plan; injects plan as assistant message. d. Tool-calling loop (up to max_iterations):
    • Calls llm.stream_complete() → yields ThinkingDelta, TextDelta, tool call requests.
    • If tool calls: executes each tool via ToolContext, yields ToolStarted → sub-agent events → ToolEvent.
    • If finish_reason == stop: yields StreamEnd, runs post-turn workers. e. Saves session to DB.
  5. Events are broadcast from SessionRun to all subscriber queues.
  6. The WebSocket handler drains its queue → sends JSON to the client.

Tool context

Defined in navi/tools/_internal/base.py.

Legacy path: ContextVars (current_session_id, current_event_sink, current_stop_event, current_model, current_user_id, current_user_role, current_user_info) are still present for backward compatibility.

Current path: Agent builds a ToolContext dataclass explicitly and passes it into every tool's execute() call. This removes hidden dependencies and makes tool execution deterministic and testable:

Field Type Purpose
session_id `str \ None` Session ID for per-session state (SSH pool, scratchpad, todo)
event_sink `Queue \ None` Queue where subagent events are written; parent drains it in real time
stop_event `Event \ None` Cooperative stop signal checked before each LLM call
model `list[str] \ str \ None` Current profile model — tools that call the LLM read this
user_id `str \ None` Authenticated user ID
user_role str Role (user or admin) — admins bypass sandbox restrictions
user_info `dict \ None` Full user profile for context injection

Registry wiring (navi/core/registry.py)

build_default_registries() is the composition root. It:

  1. Creates ToolRegistry, registers all built-in tools.
  2. Loads user tools from tools/ directory.
  3. Creates ProfileRegistry, registers all profiles from navi/profiles/.
  4. Creates BackendRegistry, registers LLM backend. If OLLAMA_BACKENDS_FILE is set, uses FallbackOllamaBackend (multi-server with blacklisting); otherwise uses the single OllamaBackend.
  5. Creates SpawnAgentTool and SwitchProfileTool (need references to other registries).
  6. Patches spawn_tool._backend_registry after backends are built (avoids circular dep).

Called once at startup from navi/api/deps.py.

Two-buffer session design

Each Session has two message lists:

  • messages — full display history, never modified by compression. Used for UI history.
  • context — what the LLM sees. May be replaced with a summary by the compressor.

See sessions.md for details.