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)¶
- Browser connects via WebSocket to
/ws/{session_id} - Server immediately sends a
modelsevent with available models - User sends
{type: "message", text: "...", model: "..."} _AuthMiddlewarevalidates the session cookie on the HTTP upgrade- Agent loop starts: builds system prompt, loads conversation history
- Provider is called with messages + tool schemas
- For each tool call the model requests:
- Security checks (whitelist, path, confirmation)
- If confirmation required:
ConfirmationRequiredEventsent to browser; loop pauses - User approves/denies via
{type: "confirm", ...} - Tool executes; result is appended to messages
- Audit record written
- When model returns no tool calls:
DoneEventsent; 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)