đī¸ Architecture
VexAI is built with TypeScript (ESM), using discord.js for Discord integration, better-sqlite3/pg for databases, and a modular skill architecture. This page covers the internal architecture, data flow, and key design decisions.
Boot Flow
When VexAI starts, index.ts checks for a configuration file. If none exists, the interactive setup wizard runs; otherwise the bot boots up and initializes every subsystem in sequence.
index.ts
âââ Check for data/config.json
â âââ Missing â Run setup wizard (src/setup/wizard.ts)
â â âââ Interactive prompts â write config.json
â âââ Found â Start bot (src/bot/client.ts)
â âââ Initialize database (SQLite or PostgreSQL)
â âââ Run migrations
â âââ Register skills (built-in + user skills)
â âââ Initialize LLM provider
â âââ Start security observer
â âââ Register slash commands
â âââ Start integrations (webhooks, RSS, email)
â âââ Start event system
â âââ Start message backfill
â âââ Ready: listening for messages
On the very first launch the wizard creates data/config.json, identity files, and the database. Subsequent starts skip the wizard entirely.
Message Pipeline
Every user message flows through message-handler.ts in a well-defined sequence. The tool execution loop can iterate up to 20 times by default, letting the LLM chain multiple tool calls in a single turn.
User Message (Discord)
â
âââ 1. Load conversation history (memory + SQLite cold tier)
âââ 2. Assemble system prompt:
â âââ Identity (IDENTITY.md, SOUL.md)
â âââ Rules (RULES.md)
â âââ Core memory (CORE_MEMORY.md)
â âââ Per-user memory (MEMORY.md)
â âââ Tool list (optimized by prompt optimizer)
âââ 3. Send to LLM provider
âââ 4. Tool execution loop (max iterations, default 20):
â âââ Parse tool calls from LLM response
â âââ For each tool call:
â â âââ Check tool cache â return cached if hit
â â âââ Security observer gate:
â â â âââ Tier 0: auto-approve
â â â âââ Tier 1: rule-based check
â â â âââ Tier 2: cached LLM verdict
â â â âââ Tier 3: full LLM review
â â âââ Approval gate (if destructive)
â â âââ Execute tool via skill registry
â â âââ Cache result
â âââ Send tool results back to LLM â repeat
âââ 5. Split response at 2000 chars (Discord limit)
âââ 6. Send reply to Discord
Identical tool calls within the same conversation turn are automatically de-duplicated via the tool cache, reducing LLM API costs and latency.
LLM Abstraction Layer
All LLM communication goes through a unified provider interface. The BaseProvider abstract class handles retry logic with 3 attempts and exponential backoff. Four concrete implementations cover every major provider.
| Provider | Class | Notes |
|---|---|---|
| OpenAI | OpenAIProvider |
Native function calling |
| Anthropic | AnthropicProvider |
Tool use via Anthropic API |
| OpenRouter | OpenRouterProvider |
Routes to any model |
| OpenAI-Compatible | OpenAICompatibleProvider |
Any /v1/chat/completions endpoint |
- Tool/message adapters normalize format differences between providers. Each provider translates tool calls and results to/from its native API format.
ProviderCachemanages per-user model overrides, letting individual users switch models without affecting others.
Security Flow
Every tool invocation passes through the security observer before execution. The tiered system balances safety with performance: low-risk tools fly through instantly while high-risk actions get full LLM review.
Tool Call Received
â
âââ Check risk tier (risk-tiers.ts)
â âââ Tier 0 â APPROVE (no overhead)
â âââ Tier 1 â Rate limit check â APPROVE/DENY
â âââ Tier 2 â Check verdict cache
â â âââ Cache hit â return cached verdict
â â âââ Cache miss â LLM review â cache â APPROVE/DENY/ESCALATE
â âââ Tier 3 â Always LLM review â APPROVE/DENY/ESCALATE
â
âââ On APPROVE â execute tool
âââ On DENY â return denial message to LLM
âââ On ESCALATE â alert channel + deny execution
If the security observer encounters an error (e.g., LLM timeout), the tool call is denied by default. This fail-closed design ensures the bot never accidentally executes an unchecked destructive action.
Skill Architecture
Skills follow a registry pattern. Each skill exposes a set of tools that the LLM can invoke during conversation.
Skill Interface
- Each
Skillhas:name,description,tools[],execute(toolName, args, context) SkillContextprovides:channelId,userId,guildId,client,executeTool()
Loading Order
- Built-in skills: registered on startup from
src/skills/built-in/ - User skills: hot-loaded from the
skills/directory at root
Internal Tool Calls
executeTool() allows skills to call other tools internally. These internal calls have a maximum recursion depth of 5 and bypass the security observer since they are trusted by design.
Internal tool calls are capped at depth 5. If a skill exceeds this limit, a SkillError is thrown to prevent infinite recursion.
Database Layer
A DatabaseAdapter interface abstracts the underlying engine. VexAI ships with two implementations:
| Engine | Library | Features |
|---|---|---|
| SQLite | better-sqlite3 | WAL mode, zero config, local-first |
| PostgreSQL | pg | Connection pooling, tsvector, pgvector |
Migrations
Sequential migrations (v1âv8 for SQLite, separate Postgres migrations) run automatically on boot. Each migration is idempotent.
Storage Services
- Conversation store: tiered memory (hot in-memory, cold in database)
- Memory store: per-user and core memory persistence
- Message mirror: full Discord message archival for search
- Embedding service: vector storage for semantic search
Error Handling
VexAI uses a custom error hierarchy rooted in ClawAIError. Each error type carries structured context for logging and debugging.
ClawAIError (base)
âââ ConfigError: configuration problems
âââ LLMError: LLM provider failures
âââ SecurityError: security violations
âââ SkillError: skill execution failures
All custom errors are defined in src/utils/errors.ts and extend the base ClawAIError class for consistent catch handling across the codebase.
Key Design Decisions
| Decision | Detail |
|---|---|
| ESM throughout | "type": "module" in package.json. All imports use .js extensions |
| TypeScript strict mode | ES2022 target, ESNext modules, bundler module resolution |
| Local-first | All searches hit local DB first, external API as fallback |
| Fail-closed security | Errors default to deny: never execute unchecked actions |
Runtime data in data/ |
Config, database, identity files, and user data all live in data/ |
| 2000 char split | Discord message limit handled transparently by src/utils/markdown.ts |