Architecture

Prax is organized in five layers with a clear direction of dependency: blueprints → services → agent → plugins (plus the legacy readers/ helpers being migrated into plugins). The hub-and-spoke architecture keeps each agent's tool count…

Synced from Prax at 35c345e5 View source ↗

Prax is organized in five layers with a clear direction of dependency: blueprints → services → agent → plugins (plus the legacy readers/ helpers being migrated into plugins). The hub-and-spoke architecture keeps each agent’s tool count low while providing deep domain capabilities.

Contents

  • Hub-and-Spoke Architecture — Orchestrator, spoke agents, sub-hubs, and delegation patterns
  • The lang* Stack — How Prax uses LangChain/LangGraph: the build_agent_loop construction seam (layer rule 4), in-loop middleware (AGENT_MIDDLEWARE_ENABLED), version policy, and the durable-resume decision gate
  • Request Flows — SMS, Discord, TeamWork, sandbox, and scheduling flows
  • Workspace — Per-user git-backed file layout, TeamWork integration, Dropbox sync
  • Interoperability frontier — Where Prax sits in the agent-interop ecosystem across five axes (expose / consume / discover / interchange / identity). Already a first-class provider (MCP server + OKF interchange); the highest-leverage gap is the consumer half — an outbound MCP/A2A client that makes any external agent a spoke. Content provenance is the natural second frontier; capability discovery (ARD) stays deferred
  • Memory System — STM scratchpad, LTM vector + graph, hybrid retrieval, consolidation

High-Level System Overview

graph TB
    User["User (Phone / Discord / Web)"]

    subgraph TeamWork["TeamWork Web UI"]
        TWChat["Chat Channels"]
        TWKanban["Kanban Board"]
        TWTerminal["Terminal"]
        TWBrowser["Browser Screencast"]
    end

    subgraph Twilio["Twilio Cloud (optional)"]
        Voice["Voice Webhook"]
        SMS["SMS Webhook"]
    end

    subgraph Discord["Discord (optional)"]
        DiscordBot["Discord Bot\n(WebSocket)"]
    end

    subgraph Flask["Flask App"]
        MainRoutes["/transcribe, /respond"]
        SmsRoute["/sms"]
        VoiceSvc["VoiceService"]
        SmsSvc["SmsService"]
        DiscordSvc["DiscordService"]
        ConvoSvc["ConversationService"]
    end

    subgraph Agent["LangGraph ReAct Agent (Prax)"]
        Orchestrator["ConversationAgent"]
        LLMFactory["LLM Factory"]

        subgraph Tools["Orchestrator Tools (45 as of 2026-09, sandbox enabled)"]
            BuiltIn["Kernel (4)\nsearch, datetime, URL fetch, sandbox_shell"]
            WS["Planning / meta / introspection (24)\nagent_plan_*, progress_*, user_notes_*, think,\nrun_python, trace_*, review_my_traces, doctor, ..."]
            Spokes["Spoke Delegates (15)\nbrowser, content_editor, course, desktop,\nenvironment, finetune, knowledge, plugins,\nprofessor, research, sandbox, scheduler,\nsysadmin, tasks, workspace"]
            SA["Sub-Agent (2)\ndelegate_task, delegate_parallel"]
        end
    end

    subgraph PluginSys["Plugin System (Hot-Swappable)"]
        PluginLoader["Plugin Loader\n(folder-per-plugin discovery)"]
        PluginSandbox["Subprocess Sandbox"]
        PluginRegistry["Version Registry\n(registry.json)"]
        PluginCatalog["CATALOG.md\n(auto-generated)"]
        PromptMgr["Prompt Manager"]
        LLMConfig["LLM Routing\n(llm_routing.yaml)"]
        BuiltInPlugins["Built-In Plugins\n(NPR, PDF, YouTube, arXiv, ...)"]
        CustomPlugins["Custom Plugins\n(plugins/tools/custom/)"]
        SysPrompt["System Prompt\n(plugins/prompts/)"]
    end

    subgraph PluginRepo["Plugin Repository (optional)"]
        RemoteRepo["Private Git Repo\n(SSH deploy key)"]
        RepoBranch["Branch: plugins"]
        RemoteCatalog["CATALOG.md"]
    end

    subgraph Storage["Persistence"]
        SQLite["SQLite\n(conversation memory)"]
        Workspace["Git-Backed Workspace\n(per-user files)"]
        ScheduleYAML["schedules.yaml\n(cron definitions)"]
        AdapterReg["adapter_registry.json\n(LoRA adapters)"]
        Todos["todos.json\n(user to-do list)"]
        Links["links.md\n(link history)"]
        Instructions["instructions.md\n(prompt reference)"]
        AgentPlan["agent_plan.yaml\n(task decomposition)"]
    end

    subgraph Memory["Memory System (optional)"]
        STM["STM Scratchpad\n(workspace JSON)"]
        Qdrant["Qdrant\n(dense + sparse vectors)"]
        Neo4j["Neo4j\n(entity graph)"]
        Embedder["Embedder\n(OpenAI / Ollama / local)"]
    end

    subgraph Sandbox["Docker Sandbox"]
        Container["Docker Container"]
        Exec["Pure exec env\n(shell · Python · DuckDB · Lean · desktop)"]
    end

    subgraph LocalML["Local ML Stack (optional)"]
        vLLM["vLLM Server\n(OpenAI-compat API)"]
        LoRA["LoRA Adapters\n(hot-swappable)"]
        Unsloth["Unsloth QLoRA\n(training subprocess)"]
    end

    subgraph Browser["Browser (Playwright)"]
        Chromium["Headless Chromium"]
        SiteCreds["sites.yaml\n(credentials)"]
        Profiles["Persistent Profiles\n(cookies/sessions)"]
        VNCServer["Xvfb + x11vnc\n(manual login)"]
    end

    Worktree["Git Worktree\n(self-modification)"]

    Scheduler["APScheduler\n(background cron)"]

    User -->|Web| TWChat
    User -->|Call| Voice
    User -->|Text| SMS
    User -->|Message| DiscordBot
    TWChat -->|Webhook| ConvoSvc
    TWTerminal -->|docker exec| Container
    TWBrowser -->|CDP| Chromium
    Voice --> MainRoutes --> VoiceSvc
    SMS --> SmsRoute --> SmsSvc
    DiscordBot --> DiscordSvc
    VoiceSvc --> ConvoSvc
    SmsSvc --> ConvoSvc
    DiscordSvc --> ConvoSvc
    ConvoSvc --> Orchestrator
    Orchestrator --> LLMFactory
    LLMFactory -->|cloud| BuiltIn
    LLMFactory -->|local| vLLM
    Orchestrator --> Tools
    WS --> Workspace
    Spokes -->|sandbox| Container
    Container --> Exec
    Spokes -->|scheduler| ScheduleYAML
    Spokes -->|finetune| Unsloth
    Spokes -->|finetune| vLLM
    vLLM --> LoRA
    Spokes -->|sysadmin → self-improve| Worktree
    Spokes -->|sysadmin| PluginLoader
    PluginLoader --> BuiltInPlugins
    PluginLoader --> CustomPlugins
    PluginLoader --> PluginSandbox
    PluginLoader --> PluginRegistry
    PluginLoader --> PluginCatalog
    CustomPlugins -.->|push| RemoteRepo
    RemoteRepo --> RepoBranch
    RepoBranch --> RemoteCatalog
    Orchestrator --> PromptMgr
    PromptMgr --> SysPrompt
    LLMFactory --> LLMConfig
    Spokes -->|browser| Chromium
    Chromium --> SiteCreds
    Chromium --> Profiles
    VNCServer --> Chromium
    ConvoSvc --> SQLite
    Scheduler -->|reads| ScheduleYAML
    Scheduler -->|fires| ConvoSvc
    ConvoSvc -->|SMS / Discord reply| User
    Worktree -->|PR| Workspace
    Orchestrator -->|context inject| STM
    Orchestrator -->|turn-end consolidation| Qdrant
    Orchestrator -->|turn-end consolidation| Neo4j
    Embedder --> Qdrant
    STM --> Workspace

Concepts — What Lives Where

Prax is organized in five layers. Each has a clear job and a single direction of dependency: blueprints → services → agent → plugins, with readers/ as a legacy fifth layer that is being folded into plugins.

Layer Directory What it is Example
Blueprints prax/blueprints/ Flask route handlers — the HTTP surface. They receive webhooks from Twilio (voice, SMS) or serve static files. Blueprints know about channels but not about the agent. POST /sms validates a Twilio signature, hands the message to SmsService, and returns a TwiML response.
Services prax/services/ Business logic that doesn’t belong in the agent. A service encapsulates one capability: workspace git ops, Docker sandbox lifecycle, Playwright browser sessions, APScheduler cron, Hugo publishing, etc. Services are called both by blueprints (channel-facing) and by agent tools (capability-facing). They never call the agent directly. workspace_service.py manages the per-user git repo — creating, reading, locking, committing.
Agent prax/agent/ The LangGraph ReAct loop and everything around it: the orchestrator, LLM factory, tool builders, governance, checkpointing. Tool builder files (*_tools.py) define groups of LangChain tools that thin-wrap a service. The agent layer decides what to do; services decide how to do it. sandbox_tools.py exposes the direct-execution tools (sandbox_shell, sandbox_install, sandbox_rebuild, sandbox_view/scroll/goto, desktop_*) that reach the container through prax/services/sandbox_bridge.py (the prax_sandbox_client adapter); delegate_sandbox runs a headless sub-agent that writes and runs code directly in the container via sandbox_shell. The multi-round OpenCode coding-session tools were removed (2026-07) — Prax codes natively.
Plugins prax/plugins/ Hot-swappable extensions discovered at startup. Each plugin lives in plugins/tools/<name>/plugin.py, exports a register() function returning LangChain tools, and can be created/modified/rolled back at runtime — by the agent itself. The plugin system also manages the system prompt and LLM routing config. plugins/tools/news/plugin.py provides the unified news tool with actions for briefings, RSS checking, and audio.
Readers prax/readers/ Legacy content-extraction helpers (ArXiv, NPR audio, web scraping). Being migrated into plugins. New code should use or create a plugin instead. readers/news/npr_top_hour.py fetches the latest NPR podcast URL — now called by the news plugin.

How they connect:

User ──▸ Twilio/Discord
            │
        Blueprints          (HTTP layer — routes, auth)
            │
        Services             (business logic — workspace, sandbox, browser, scheduler, …)
            │
        Agent                (LangGraph ReAct loop — orchestrator, tools, governance)
            │
        Plugins              (hot-swappable tools — news, PDF, YouTube, custom, …)

Rules of thumb:

  • Need a new channel? Add a blueprint + a channel service.
  • Need a new capability (e.g., email sending)? Add a service, then wrap it with a tool builder in agent/ or a plugin in plugins/tools/.
  • Need a new tool the agent can call? If it’s a core, always-on tool, add it to an agent/*_tools.py builder. If it’s optional, content-focused, or user-modifiable, make it a plugin.
  • Need to change the system prompt? Edit plugins/prompts/system_prompt.md (or let the agent do it at runtime via prompt_write).

Memory System

Prax has a two-layer memory system inspired by human cognition, implemented across three data stores:

┌─────────────────────────────────────────────────────┐
│                  Orchestrator                        │
│  (injects STM + LTM context into system prompt)     │
└────────────┬────────────────────────┬───────────────┘
             │                        │
     ┌───────▼───────┐       ┌───────▼───────────┐
     │  STM (fast)   │       │  LTM (durable)    │
     │  Workspace    │       │  ┌─────────────┐  │
     │  JSON file    │       │  │ Qdrant      │  │
     │  (always on)  │       │  │ dense+sparse│  │
     │               │       │  └──────┬──────┘  │
     │  key → value  │       │         │ RRF     │
     │  + importance │       │  ┌──────┴──────┐  │
     │  + tags       │       │  │ Neo4j       │  │
     │               │       │  │ entities    │  │
     └───────────────┘       │  │ temporals   │  │
                             │  │ causals     │  │
                             │  └─────────────┘  │
                             └───────────────────┘
                                      │
                             ┌────────▼────────┐
                             │ Consolidation   │
                             │ (scheduled)     │
                             │ traces → LLM    │
                             │ → conf gate≥0.6 │
                             │ → graph + vector│
                             │ → dual decay    │
                             └─────────────────┘

Short-term memory (STM) is a per-user JSON scratchpad stored in the workspace ({workspace}/memory/stm.json). It requires no infrastructure — works even when Qdrant/Neo4j are unreachable. The orchestrator injects STM entries into the system prompt on every turn.

Long-term memory (LTM) requires Qdrant (vector store) and Neo4j (knowledge graph) — both are compiled into the prax Docker image and start with the container (no compose profile; make run-local-all starts them as containers for a host-process Prax). Memories are stored as both dense embeddings (semantic similarity) and sparse TF-IDF vectors (keyword matching) in Qdrant, plus entities, typed relations, temporal events, and causal links in Neo4j (multi-graph separation). At query time, all three retrieval arms (dense, sparse, graph neighbourhood) run in parallel and results are fused via query-adaptive weighted RRF — factual queries boost sparse+graph, semantic queries boost dense.

Consolidation converts conversation traces into durable memories: LLM extraction of entities/relations/facts/temporal events/causal links → confidence validation gate (≥0.6 → LTM, below → STM pending review) → graph upsert with bi-temporal edges (valid_from/valid_until for supersession tracking) → vector upsert → dual decay (Ebbinghaus time-based + interaction-based, using the stronger signal) → daily summaries.

Embedding providers are pluggable: OpenAI (default, cloud), Ollama (local, no data leaves your machine), or fastembed (in-process, zero dependencies). The system gracefully degrades — if Qdrant or Neo4j are unreachable, LTM returns empty results without errors. STM always works.

A memory spoke (prax/agent/spokes/memory/) with explicit remember/recall/forget/graph tools exists but is deliberately not registered with the orchestrator (see the note in prax/agent/spokes/__init__.py): memory writes happen automatically through the turn-end consolidation hook and reads through the per-turn context injection. See Memory deep-dive for full documentation.