Skip to content

Architecture Overview

Component map

┌──────────────────────────────────────────────────────────┐
│                     Browser / Client                     │
└────────────────────────────┬─────────────────────────────┘
                             │ HTTP / WebSocket
┌────────────────────────────▼─────────────────────────────┐
│                    FastAPI Application                    │
│                                                          │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────────┐  │
│  │  HTML Pages  │  │  REST API    │  │  WebSocket    │  │
│  │  (Jinja2)    │  │  /api/*      │  │  /ws/{id}     │  │
│  └──────────────┘  └──────────────┘  └───────┬───────┘  │
│                                              │           │
│  ┌───────────────────────────────────────────▼────────┐  │
│  │                    Agent Loop                      │  │
│  │  system prompt → provider call → tool dispatch    │  │
│  │  → confirmation → audit → next turn               │  │
│  └──────────────────────────┬───────────────────────┘   │
│                             │                            │
│  ┌──────────────────────────▼───────────────────────┐   │
│  │            Tool Registry (15 tools)              │   │
│  │  email / caldav / web / filesystem / browser     │   │
│  │  pushover / telegram / brain / image_gen / ...   │   │
│  └──────────────────────────────────────────────────┘   │
│                                                          │
│  ┌───────────────────────┐  ┌──────────────────────────┐ │
│  │  APScheduler          │  │  Background Listeners    │ │
│  │  (cron agents)        │  │  email / telegram / RSS  │ │
│  │                       │  │  page monitors           │ │
│  └───────────────────────┘  └──────────────────────────┘ │
│                                                          │
└────────────────────────────┬─────────────────────────────┘
          ┌──────────────────┴──────────────────────┐
          │               PostgreSQL                 │
          │  aide DB (schema v28) · brain DB (pgvec) │
          └─────────────────────────────────────────┘

Key design decisions

Why FastAPI?

FastAPI was chosen for its native async/await support (critical for the WebSocket streaming agent loop), automatic OpenAPI generation, and typed dependency injection. All database calls, provider calls, and tool executions are async — the server never blocks on I/O.

Why PostgreSQL?

The project started with SQLite but migrated to PostgreSQL for: - The pgvector extension (semantic search in the 2nd Brain) - Better concurrent write handling for multi-user operation - asyncpg connection pooling for high-throughput async I/O - JSONB columns for flexible tool schemas and conversation messages

Credentials remain encrypted at the application level (AES-256-GCM) — PostgreSQL-level encryption is not used because the encryption key is DB_MASTER_PASSWORD, not the DB password itself.

Why in-process scheduling?

APScheduler runs inside the FastAPI process on the same event loop. This means: - No separate Celery workers to deploy and monitor - Agents share the same connection pool and in-memory state - The tradeoff is that heavy agent runs compete with HTTP requests — mitigated by the concurrency semaphore (system:max_concurrent_runs)

Why asyncio.ContextVar for request state?

Tools need context (session ID, user folder, web tier flag) that varies per request. Thread-local variables don't work in async code. ContextVar values are automatically copied into child tasks but don't leak between concurrent requests — the correct mechanism for async request-scoped state.


Module map

Package Responsibility
server/main.py FastAPI app, WebSocket handler, auth middleware, lifespan
server/config.py Env loading, Settings singleton, agent name extraction
server/database.py asyncpg pool, migrations, credential/whitelist/audit stores
server/auth.py Argon2 hashing, HMAC session cookies, TOTP
server/users.py User CRUD, folder provisioning
server/security.py Whitelist enforcement, path sandboxing, sanitisation
server/security_screening.py Optional: canary tokens, LLM screening, output validation
server/audit.py Append-only audit log with filtering
server/context_vars.py asyncio ContextVars for per-request state
server/agent/agent.py Core agent loop, event types, conversation persistence
server/agent/tool_registry.py Tool registration, schema generation, dispatch
server/agent/confirmation.py Pause/resume flow for confirmable tool calls
server/agents/runner.py AgentRunner: APScheduler integration, concurrency
server/agents/tasks.py Agent + run CRUD
server/tools/ 15 tool implementations (see Tools)
server/providers/ AI provider abstraction (Anthropic, OpenRouter, OpenAI)
server/web/routes.py REST API routes
server/web/themes.py UI theme support
server/brain/ 2nd Brain: pgvector embeddings, search, ingestion
server/inbox/ Email inbox listeners, trigger dispatch
server/telegram/ Telegram bot long-polling, trigger dispatch
server/monitors/ RSS and page-change monitors
server/mcp_client/ MCP server discovery and tool proxying
server/webhooks/ Inbound webhook endpoint handling

Request lifecycle (chat)

  1. Browser connects via WebSocket to /ws/{session_id}
  2. Server immediately sends a models event with available models
  3. User sends {type: "message", text: "...", model: "..."}
  4. _AuthMiddleware validates the session cookie on the HTTP upgrade
  5. Agent loop starts: builds system prompt, loads conversation history
  6. Provider is called with messages + tool schemas
  7. For each tool call the model requests:
  8. Security checks (whitelist, path, confirmation)
  9. If confirmation required: ConfirmationRequiredEvent sent to browser; loop pauses
  10. User approves/denies via {type: "confirm", ...}
  11. Tool executes; result is appended to messages
  12. Audit record written
  13. When model returns no tool calls: DoneEvent sent; conversation saved to DB

Data flow: tool call

Agent loop
  ├─ yield ToolStartEvent (→ browser shows spinner)
  ├─ tool.should_confirm(**args)
  │     ├─ True  → yield ConfirmationRequiredEvent
  │     │              wait for confirmation_manager.request()
  │     │              ├─ approved → continue
  │     │              └─ denied  → append error result, yield ToolDoneEvent(success=False)
  │     └─ False → execute directly
  ├─ registry.dispatch(name, arguments)   [catches all exceptions]
  │     └─ tool.execute(**args) → ToolResult
  ├─ audit_log.record(...)
  ├─ messages.append({role: "tool", content: result_json})
  └─ yield ToolDoneEvent (→ browser hides spinner, shows result)