/`
+5. Add optional dependencies to `pyproject.toml` if needed
diff --git a/docs/development/roadmap.md b/docs/development/roadmap.md
new file mode 100644
index 00000000..d98a7d4b
--- /dev/null
+++ b/docs/development/roadmap.md
@@ -0,0 +1,85 @@
+# Roadmap
+
+OpenJarvis development follows a phased approach, with each version adding
+a major pillar or cross-cutting capability to the framework.
+
+---
+
+## Development Phases
+
+| Version | Phase | Status | Delivers |
+|---|---|---|---|
+| **v0.1** | Phase 0 -- Scaffolding | :material-check-circle:{ .green } Complete | Project scaffolding, registry system (`RegistryBase[T]`), core types (`Message`, `ModelSpec`, `Conversation`, `ToolResult`), configuration loader with hardware detection, Click CLI skeleton |
+| **v0.2** | Phase 1 -- Intelligence + Inference | :material-check-circle:{ .green } Complete | Intelligence pillar (model catalog, heuristic router), inference engines (Ollama, vLLM, llama.cpp), engine discovery and health probing, `jarvis ask` command working end-to-end |
+| **v0.3** | Phase 2 -- Memory | :material-check-circle:{ .green } Complete | Memory backends (SQLite/FTS5, FAISS, ColBERTv2, BM25, Hybrid/RRF), document chunking and ingestion pipeline, context injection with source attribution, `jarvis memory` commands |
+| **v0.4** | Phase 3 -- Agents + Tools + Server | :material-check-circle:{ .green } Complete | Agent system (SimpleAgent, OrchestratorAgent, OpenClawAgent, CustomAgent), tool system (Calculator, Think, Retrieval, LLM, FileRead), ToolExecutor dispatch engine, OpenAI-compatible API server (`jarvis serve`) |
+| **v0.5** | Phase 4 -- Learning + Telemetry | :material-check-circle:{ .green } Complete | Learning system (HeuristicRouter policy, TraceDrivenPolicy, GRPO stub), reward functions, telemetry aggregation (per-model/engine stats, export), `--router` CLI flag, `jarvis telemetry` commands |
+| **v1.0** | Phase 5 -- SDK + Production | :material-check-circle:{ .green } Complete | Python SDK (`Jarvis` class, `MemoryHandle`), OpenClaw agent infrastructure (protocol, transports, plugins), benchmarking framework (latency, throughput), Docker deployment (CPU + GPU), MkDocs documentation site |
+| **v1.1** | Phase 6 -- Traces + Learning | :material-progress-clock:{ .amber } In Progress | Trace system (`TraceStore`, `TraceCollector`, `TraceAnalyzer`), trace-driven learning, pluggable agent architectures (ReAct, OpenHands), MCP integration layer |
+
+---
+
+## Current Status
+
+OpenJarvis v1.0 is complete. The framework provides:
+
+- **Four core abstractions** -- Intelligence, Engine, Agentic Logic, Memory -- each with an ABC interface and registry-based discovery
+- **Five inference engines** -- Ollama, vLLM, llama.cpp, SGLang, Cloud (OpenAI/Anthropic/Google)
+- **Five memory backends** -- SQLite/FTS5, FAISS, ColBERTv2, BM25, Hybrid (RRF fusion)
+- **Multiple agent types** -- Simple, Orchestrator, Custom, OpenClaw, ReAct, OpenHands
+- **Seven built-in tools** -- Calculator, Think, Retrieval, LLM, FileRead, WebSearch, CodeInterpreter
+- **Python SDK** -- `Jarvis` class for programmatic use
+- **OpenAI-compatible API server** -- `POST /v1/chat/completions`, `GET /v1/models`
+- **Benchmarking framework** -- Latency and throughput measurements
+- **Telemetry and traces** -- SQLite-backed recording and aggregation
+- **Docker deployment** -- CPU and GPU images with docker-compose
+
+Phase 6 is actively in progress, adding the trace system and trace-driven
+learning capabilities.
+
+---
+
+## Phase 6 Details
+
+Phase 6 focuses on closing the loop between execution and learning:
+
+### Trace System
+
+- **TraceStore** -- Persists complete `Trace` objects to SQLite, capturing the
+ full sequence of steps (route, retrieve, generate, tool_call, respond) with
+ timing, inputs, outputs, and outcomes
+- **TraceCollector** -- Wraps any `BaseAgent` to automatically record traces
+ during execution via EventBus subscription
+- **TraceAnalyzer** -- Read-only query layer providing aggregated statistics
+ (per-route, per-tool, by query type, time-range filtering)
+
+### Trace-Driven Learning
+
+- **TraceDrivenPolicy** -- A router policy that learns from historical trace
+ outcomes to improve model selection over time
+- Query classification groups traces by type (code, math, short, long, general)
+- Per-model scoring combines success rate and user feedback
+- Online updates via `observe()` for incremental learning
+
+### Pluggable Agents
+
+- **ReActAgent** -- Reasoning + Acting pattern for systematic tool use
+- **OpenHands** -- Integration with the OpenHands agent framework
+
+---
+
+## Future Directions
+
+Beyond Phase 6, areas of ongoing exploration include:
+
+- **GRPO training** -- Reinforcement learning from trace data to train the
+ routing policy, moving beyond heuristics and simple statistics
+- **Streaming telemetry** -- Real-time performance dashboards and alerting
+- **Multi-model orchestration** -- Coordinating multiple models within a
+ single query pipeline (e.g., small model for classification, large model
+ for generation)
+- **Federated memory** -- Memory backends that synchronize across devices
+- **Plugin ecosystem** -- Community-contributed engines, tools, and agents
+ distributed as Python packages
+- **Energy-aware routing** -- Using power consumption data from telemetry to
+ optimize for energy efficiency alongside latency and quality
diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md
new file mode 100644
index 00000000..b3a3a575
--- /dev/null
+++ b/docs/getting-started/configuration.md
@@ -0,0 +1,561 @@
+---
+title: Configuration
+description: Complete reference for OpenJarvis configuration
+---
+
+# Configuration
+
+OpenJarvis uses a TOML configuration file to control engine selection, model routing, memory backends, agent behavior, and more. This page is the complete reference for every configuration option.
+
+## Config File Location
+
+The configuration file lives at:
+
+```
+~/.openjarvis/config.toml
+```
+
+OpenJarvis creates the `~/.openjarvis/` directory and populates it with a default config when you run `jarvis init`.
+
+## Generating Configuration
+
+### First-Time Setup
+
+```bash
+jarvis init
+```
+
+This command:
+
+1. Runs hardware auto-detection (GPU vendor/model/VRAM, CPU brand/cores, RAM)
+2. Selects the recommended engine based on your hardware
+3. Writes `~/.openjarvis/config.toml` with sensible defaults
+
+### Regenerating Configuration
+
+To overwrite an existing config:
+
+```bash
+jarvis init --force
+```
+
+!!! warning
+ `--force` overwrites your existing config file. Back up your config first if you have custom settings.
+
+## Configuration Sections
+
+The config file is organized into seven TOML sections. Every field has a default value, so you only need to specify the values you want to change.
+
+---
+
+### `[engine]` -- Inference Engine
+
+Controls which inference engine is used and where each engine is listening.
+
+```toml
+[engine]
+default = "ollama"
+ollama_host = "http://localhost:11434"
+vllm_host = "http://localhost:8000"
+llamacpp_host = "http://localhost:8080"
+llamacpp_path = ""
+sglang_host = "http://localhost:30000"
+```
+
+| Field | Type | Default | Description |
+|-------|------|---------|-------------|
+| `default` | string | Auto-detected | Default engine backend. One of: `ollama`, `vllm`, `llamacpp`, `sglang`, `cloud`. Set automatically by `jarvis init` based on hardware detection. |
+| `ollama_host` | string | `http://localhost:11434` | Base URL for the Ollama API server. |
+| `vllm_host` | string | `http://localhost:8000` | Base URL for the vLLM OpenAI-compatible server. |
+| `llamacpp_host` | string | `http://localhost:8080` | Base URL for the llama.cpp HTTP server (`llama-server`). |
+| `llamacpp_path` | string | `""` | Path to the llama.cpp binary, if not on `$PATH`. |
+| `sglang_host` | string | `http://localhost:30000` | Base URL for the SGLang server. |
+
+!!! tip "Engine fallback"
+ If the configured default engine is unreachable, OpenJarvis automatically probes all registered engines and falls back to any healthy one.
+
+---
+
+### `[intelligence]` -- Model Routing
+
+Controls which model is selected by default and which model to fall back to.
+
+```toml
+[intelligence]
+default_model = ""
+fallback_model = ""
+```
+
+| Field | Type | Default | Description |
+|-------|------|---------|-------------|
+| `default_model` | string | `""` (empty) | Preferred model identifier (e.g., `qwen3:8b`). When empty, the router policy selects the model dynamically. |
+| `fallback_model` | string | `""` (empty) | Model to use if the default is unavailable. |
+
+When both fields are empty, OpenJarvis uses the configured router policy (see `[learning]`) to select a model from those available on the active engine.
+
+---
+
+### `[learning]` -- Router Policy
+
+Controls how the learning system selects models for incoming queries.
+
+```toml
+[learning]
+default_policy = "heuristic"
+reward_weights = ""
+```
+
+| Field | Type | Default | Description |
+|-------|------|---------|-------------|
+| `default_policy` | string | `"heuristic"` | Router policy to use for model selection. Available: `heuristic`, `learned` (trace-driven), `grpo` (reinforcement learning stub). |
+| `reward_weights` | string | `""` | Comma-separated key=value pairs for the reward function. Example: `"latency=0.4,cost=0.3,quality=0.3"`. |
+
+**Router policies:**
+
+| Policy | Description |
+|--------|-------------|
+| `heuristic` | Rule-based selection using 6 priority rules. Considers model availability, parameter count, context length, and query characteristics. Default. |
+| `learned` | Trace-driven policy that learns from past interaction outcomes stored in the trace system. |
+| `grpo` | Group Relative Policy Optimization stub for future RL-based routing. |
+
+You can also override the router policy per-query via the CLI:
+
+```bash
+jarvis ask --router heuristic "Hello"
+```
+
+---
+
+### `[memory]` -- Memory Backend
+
+Controls the persistent memory system: which backend to use, how documents are chunked, and how context injection works.
+
+```toml
+[memory]
+default_backend = "sqlite"
+db_path = "~/.openjarvis/memory.db"
+context_injection = true
+context_top_k = 5
+context_min_score = 0.1
+context_max_tokens = 2048
+chunk_size = 512
+chunk_overlap = 64
+```
+
+| Field | Type | Default | Description |
+|-------|------|---------|-------------|
+| `default_backend` | string | `"sqlite"` | Memory backend to use. Available: `sqlite` (FTS5), `faiss`, `colbert`, `bm25`, `hybrid`. |
+| `db_path` | string | `~/.openjarvis/memory.db` | Path to the SQLite memory database. Used by the `sqlite` backend. |
+| `context_injection` | bool | `true` | Whether to automatically inject relevant memory context into queries. |
+| `context_top_k` | int | `5` | Number of top memory results to inject as context. |
+| `context_min_score` | float | `0.1` | Minimum relevance score for a memory result to be included in context. |
+| `context_max_tokens` | int | `2048` | Maximum number of tokens to use for injected context. |
+| `chunk_size` | int | `512` | Size of document chunks (in tokens) when indexing documents. |
+| `chunk_overlap` | int | `64` | Overlap between adjacent chunks (in tokens) when indexing. |
+
+**Memory backends:**
+
+| Backend | Extra Required | Description |
+|---------|---------------|-------------|
+| `sqlite` | None | SQLite with FTS5 full-text search. Zero dependencies. Default. |
+| `faiss` | `memory-faiss` | Facebook AI Similarity Search with sentence-transformer embeddings. |
+| `colbert` | `memory-colbert` | ColBERTv2 late-interaction retrieval. Requires PyTorch. |
+| `bm25` | `memory-bm25` | BM25 sparse retrieval via `rank-bm25`. |
+| `hybrid` | Depends on sub-backends | Reciprocal Rank Fusion combining multiple backends. |
+
+!!! note "Context injection"
+ When `context_injection` is enabled and documents have been indexed, every query automatically searches memory for relevant chunks and prepends them as system context. This gives the model access to your indexed knowledge base without any extra steps. Disable with `--no-context` on the CLI or `context=False` in the SDK.
+
+---
+
+### `[agent]` -- Agent Defaults
+
+Controls the default agent behavior for queries.
+
+```toml
+[agent]
+default_agent = "simple"
+max_turns = 10
+default_tools = ""
+temperature = 0.7
+max_tokens = 1024
+```
+
+| Field | Type | Default | Description |
+|-------|------|---------|-------------|
+| `default_agent` | string | `"simple"` | Default agent to use. Available: `simple`, `orchestrator`, `custom`, `openclaw`. |
+| `max_turns` | int | `10` | Maximum number of tool-calling turns for the orchestrator agent before it must produce a final answer. |
+| `default_tools` | string | `""` | Comma-separated list of tools to enable by default (e.g., `"calculator,think"`). |
+| `temperature` | float | `0.7` | Default sampling temperature for generation. |
+| `max_tokens` | int | `1024` | Default maximum tokens for generation. |
+
+---
+
+### `[server]` -- API Server
+
+Controls the OpenAI-compatible API server started by `jarvis serve`.
+
+```toml
+[server]
+host = "0.0.0.0"
+port = 8000
+agent = "orchestrator"
+model = ""
+workers = 1
+```
+
+| Field | Type | Default | Description |
+|-------|------|---------|-------------|
+| `host` | string | `"0.0.0.0"` | Bind address for the server. Use `"127.0.0.1"` to restrict to localhost. |
+| `port` | int | `8000` | Port number for the server. |
+| `agent` | string | `"orchestrator"` | Agent to use for non-streaming chat completion requests. |
+| `model` | string | `""` | Default model for the server. When empty, uses `intelligence.default_model` or the first available model. |
+| `workers` | int | `1` | Number of uvicorn worker processes. |
+
+CLI options override config values:
+
+```bash
+jarvis serve --host 127.0.0.1 --port 9000 --model qwen3:8b --agent simple
+```
+
+---
+
+### `[telemetry]` -- Telemetry Persistence
+
+Controls whether inference telemetry is recorded and where it is stored.
+
+```toml
+[telemetry]
+enabled = true
+db_path = "~/.openjarvis/telemetry.db"
+```
+
+| Field | Type | Default | Description |
+|-------|------|---------|-------------|
+| `enabled` | bool | `true` | Whether to record telemetry for each inference call. Records timing, token counts, model, engine, and cost. |
+| `db_path` | string | `~/.openjarvis/telemetry.db` | Path to the SQLite telemetry database. |
+
+!!! info "Telemetry is local-only"
+ All telemetry data is stored locally in a SQLite database. No data is ever sent to external services.
+
+---
+
+## Hardware Auto-Detection
+
+When you run `jarvis init`, OpenJarvis probes your system to detect available hardware. The detection runs in this order:
+
+### GPU Detection
+
+1. **NVIDIA GPU** -- Checks for `nvidia-smi` on `$PATH`. If found, queries GPU name, VRAM (in MB), and GPU count via:
+
+ ```
+ nvidia-smi --query-gpu=name,memory.total,count --format=csv,noheader,nounits
+ ```
+
+2. **AMD GPU** -- Checks for `rocm-smi` on `$PATH`. If found, queries the product name via:
+
+ ```
+ rocm-smi --showproductname
+ ```
+
+3. **Apple Silicon** -- On macOS only. Runs `system_profiler SPDisplaysDataType` and looks for "Apple" in the chipset model line.
+
+If none of these detect a GPU, the system is treated as CPU-only.
+
+### CPU and RAM Detection
+
+- **CPU brand**: Reads from `sysctl -n machdep.cpu.brand_string` on macOS, or parses `model name` from `/proc/cpuinfo` on Linux.
+- **CPU count**: Uses Python's `os.cpu_count()`.
+- **RAM**: Reads from `sysctl -n hw.memsize` on macOS, or parses `MemTotal` from `/proc/meminfo` on Linux.
+
+### Detected Hardware Dataclass
+
+The detection result is stored as a `HardwareInfo` dataclass:
+
+```python
+@dataclass
+class HardwareInfo:
+ platform: str # "linux", "darwin", "windows"
+ cpu_brand: str # e.g., "AMD EPYC 7763"
+ cpu_count: int # e.g., 128
+ ram_gb: float # e.g., 512.0
+ gpu: GpuInfo | None
+
+@dataclass
+class GpuInfo:
+ vendor: str # "nvidia", "amd", "apple"
+ name: str # e.g., "NVIDIA A100-SXM4-80GB"
+ vram_gb: float # e.g., 80.0
+ compute_capability: str # (NVIDIA only)
+ count: int # e.g., 8
+```
+
+---
+
+## Engine Recommendation Logic
+
+Based on the detected hardware, `recommend_engine()` selects the optimal default engine:
+
+```mermaid
+graph TD
+ A[detect_hardware] --> B{GPU detected?}
+ B -->|No| C[llamacpp]
+ B -->|Yes| D{GPU vendor?}
+ D -->|Apple| E[ollama]
+ D -->|NVIDIA| F{Datacenter GPU?}
+ D -->|AMD| G[vllm]
+ F -->|Yes: A100, H100, H200, L40, A10, A30| H[vllm]
+ F -->|No: consumer GPU| I[ollama]
+```
+
+| Hardware | Recommended Engine | Reason |
+|----------|--------------------|--------|
+| No GPU | `llamacpp` | Efficient CPU inference with GGUF quantized models |
+| Apple Silicon | `ollama` | Native Metal acceleration, easy model management |
+| NVIDIA consumer GPU (RTX 3090, 4090, etc.) | `ollama` | Simple setup, good performance for single-user |
+| NVIDIA datacenter GPU (A100, H100, H200, L40, A10, A30) | `vllm` | High-throughput batched serving, continuous batching |
+| AMD GPU | `vllm` | ROCm support via vLLM |
+
+---
+
+## Example Configurations
+
+### Apple Silicon Mac
+
+```toml
+# ~/.openjarvis/config.toml
+# Apple Silicon MacBook Pro (M3 Max, 128 GB unified memory)
+
+[engine]
+default = "ollama"
+ollama_host = "http://localhost:11434"
+
+[intelligence]
+default_model = "qwen3:8b"
+fallback_model = "llama3.2:3b"
+
+[memory]
+default_backend = "sqlite"
+context_injection = true
+context_top_k = 5
+
+[agent]
+default_agent = "simple"
+max_turns = 10
+
+[server]
+host = "127.0.0.1"
+port = 8000
+agent = "orchestrator"
+
+[learning]
+default_policy = "heuristic"
+
+[telemetry]
+enabled = true
+```
+
+### NVIDIA Datacenter (Multi-GPU)
+
+```toml
+# ~/.openjarvis/config.toml
+# 8x NVIDIA A100 80GB server
+
+[engine]
+default = "vllm"
+vllm_host = "http://localhost:8000"
+ollama_host = "http://localhost:11434"
+
+[intelligence]
+default_model = "Qwen/Qwen2.5-72B-Instruct"
+fallback_model = "Qwen/Qwen2.5-7B-Instruct"
+
+[memory]
+default_backend = "faiss"
+context_injection = true
+context_top_k = 10
+context_min_score = 0.05
+context_max_tokens = 4096
+chunk_size = 1024
+chunk_overlap = 128
+
+[agent]
+default_agent = "orchestrator"
+max_turns = 15
+default_tools = "calculator,think,retrieval"
+temperature = 0.5
+max_tokens = 4096
+
+[server]
+host = "0.0.0.0"
+port = 8000
+agent = "orchestrator"
+model = "Qwen/Qwen2.5-72B-Instruct"
+workers = 1
+
+[learning]
+default_policy = "heuristic"
+
+[telemetry]
+enabled = true
+```
+
+### CPU-Only (No GPU)
+
+```toml
+# ~/.openjarvis/config.toml
+# CPU-only machine
+
+[engine]
+default = "llamacpp"
+llamacpp_host = "http://localhost:8080"
+
+[intelligence]
+default_model = ""
+fallback_model = ""
+
+[memory]
+default_backend = "sqlite"
+context_injection = true
+context_top_k = 3
+context_max_tokens = 1024
+chunk_size = 256
+chunk_overlap = 32
+
+[agent]
+default_agent = "simple"
+max_turns = 5
+temperature = 0.7
+max_tokens = 512
+
+[server]
+host = "127.0.0.1"
+port = 8000
+
+[learning]
+default_policy = "heuristic"
+
+[telemetry]
+enabled = true
+```
+
+### Cloud-Only (No Local Engine)
+
+```toml
+# ~/.openjarvis/config.toml
+# Using cloud APIs only (OpenAI, Anthropic)
+# Set OPENAI_API_KEY and/or ANTHROPIC_API_KEY environment variables
+
+[engine]
+default = "cloud"
+
+[intelligence]
+default_model = "gpt-4o"
+fallback_model = "claude-sonnet-4-20250514"
+
+[memory]
+default_backend = "sqlite"
+context_injection = true
+
+[agent]
+default_agent = "orchestrator"
+default_tools = "calculator,think"
+
+[telemetry]
+enabled = true
+```
+
+### Hybrid (Local + Cloud Fallback)
+
+```toml
+# ~/.openjarvis/config.toml
+# Local Ollama as primary, cloud as fallback
+
+[engine]
+default = "ollama"
+ollama_host = "http://localhost:11434"
+
+[intelligence]
+default_model = "qwen3:8b"
+fallback_model = "gpt-4o-mini"
+
+[memory]
+default_backend = "sqlite"
+context_injection = true
+
+[agent]
+default_agent = "orchestrator"
+max_turns = 10
+default_tools = "calculator,think"
+
+[learning]
+default_policy = "heuristic"
+
+[telemetry]
+enabled = true
+```
+
+---
+
+## Programmatic Configuration
+
+You can also configure OpenJarvis entirely from Python without a TOML file:
+
+```python
+from openjarvis import Jarvis
+from openjarvis.core.config import (
+ AgentConfig,
+ EngineConfig,
+ IntelligenceConfig,
+ JarvisConfig,
+ MemoryConfig,
+)
+
+config = JarvisConfig(
+ engine=EngineConfig(
+ default="ollama",
+ ollama_host="http://my-server:11434",
+ ),
+ intelligence=IntelligenceConfig(
+ default_model="qwen3:8b",
+ ),
+ memory=MemoryConfig(
+ default_backend="sqlite",
+ context_injection=True,
+ context_top_k=10,
+ ),
+ agent=AgentConfig(
+ default_agent="orchestrator",
+ max_turns=15,
+ ),
+)
+
+j = Jarvis(config=config)
+response = j.ask("Hello")
+j.close()
+```
+
+Or load from a custom path:
+
+```python
+j = Jarvis(config_path="/path/to/my-config.toml")
+```
+
+---
+
+## Environment Variables
+
+OpenJarvis respects the following environment variables:
+
+| Variable | Description |
+|----------|-------------|
+| `OPENAI_API_KEY` | API key for OpenAI cloud inference. Required for the `cloud` engine with OpenAI models. |
+| `ANTHROPIC_API_KEY` | API key for Anthropic cloud inference. Required for the `cloud` engine with Claude models. |
+| `GOOGLE_API_KEY` | API key for Google Gemini inference. Required for the `google` engine. |
+| `TAVILY_API_KEY` | API key for the Tavily web search tool. Required for the `web_search` tool. |
+
+## Next Steps
+
+- [Quick Start](quickstart.md) -- Run your first query
+- [CLI Reference](../user-guide/cli.md) -- Full reference for all CLI commands
+- [Architecture Overview](../architecture/overview.md) -- Understand how the pieces fit together
diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md
new file mode 100644
index 00000000..9647253a
--- /dev/null
+++ b/docs/getting-started/installation.md
@@ -0,0 +1,251 @@
+---
+title: Installation
+description: Install OpenJarvis and set up an inference backend
+---
+
+# Installation
+
+This guide covers installing OpenJarvis, its optional extras, and setting up an inference backend.
+
+## Requirements
+
+| Requirement | Version | Notes |
+|-------------|---------|-------|
+| Python | 3.10+ | Required |
+| Inference backend | Any | At least one of Ollama, vLLM, llama.cpp, SGLang, or a cloud API |
+| Node.js | 22+ | Only required for OpenClaw agent infrastructure |
+
+## Installing OpenJarvis
+
+=== "uv (recommended)"
+
+ ```bash
+ uv pip install openjarvis
+ ```
+
+=== "pip"
+
+ ```bash
+ pip install openjarvis
+ ```
+
+=== "From source"
+
+ ```bash
+ git clone https://github.com/jonsaadfalcon/OpenJarvis.git
+ cd OpenJarvis
+ uv sync
+ ```
+
+ For development with all dev tools:
+
+ ```bash
+ uv sync --extra dev
+ ```
+
+## Optional Extras
+
+OpenJarvis uses optional extras to keep the base installation lightweight. Install only what you need.
+
+### Inference Backends
+
+| Extra | Install Command | Dependencies | Description |
+|-------|----------------|--------------|-------------|
+| `inference-ollama` | `pip install 'openjarvis[inference-ollama]'` | None (HTTP-based) | Ollama backend. Communicates via HTTP API. |
+| `inference-vllm` | `pip install 'openjarvis[inference-vllm]'` | None (HTTP-based) | vLLM backend. Communicates via OpenAI-compatible API. |
+| `inference-llamacpp` | `pip install 'openjarvis[inference-llamacpp]'` | None (HTTP-based) | llama.cpp server backend. |
+| `inference-cloud` | `pip install 'openjarvis[inference-cloud]'` | `openai>=1.30`, `anthropic>=0.30` | Cloud inference via OpenAI and Anthropic APIs. |
+| `inference-google` | `pip install 'openjarvis[inference-google]'` | `google-genai>=1.0` | Google Gemini API backend. |
+
+!!! note "Ollama, vLLM, and llama.cpp are HTTP-based"
+ The `inference-ollama`, `inference-vllm`, and `inference-llamacpp` extras have no additional Python dependencies. OpenJarvis communicates with these engines over HTTP using the `httpx` library that is already a core dependency. You still need the actual engine software running on your machine or network.
+
+### Memory Backends
+
+| Extra | Install Command | Dependencies | Description |
+|-------|----------------|--------------|-------------|
+| `memory-faiss` | `pip install 'openjarvis[memory-faiss]'` | `faiss-cpu>=1.7`, `sentence-transformers>=2.2`, `numpy>=1.24` | FAISS vector store with sentence-transformer embeddings. |
+| `memory-colbert` | `pip install 'openjarvis[memory-colbert]'` | `colbert-ai>=0.2`, `torch>=2.0` | ColBERTv2 late-interaction retrieval. |
+| `memory-bm25` | `pip install 'openjarvis[memory-bm25]'` | `rank-bm25>=0.2.2` | BM25 sparse retrieval backend. |
+| `memory-pdf` | `pip install 'openjarvis[memory-pdf]'` | `pdfplumber>=0.10` | PDF document ingestion support. |
+
+!!! tip "SQLite memory is always available"
+ The default SQLite/FTS5 memory backend requires no additional dependencies. It is always available and suitable for most use cases.
+
+### Tools
+
+| Extra | Install Command | Dependencies | Description |
+|-------|----------------|--------------|-------------|
+| `tools-search` | `pip install 'openjarvis[tools-search]'` | `tavily-python>=0.3` | Web search tool via the Tavily API. |
+
+### Server
+
+| Extra | Install Command | Dependencies | Description |
+|-------|----------------|--------------|-------------|
+| `server` | `pip install 'openjarvis[server]'` | `fastapi>=0.110`, `uvicorn>=0.30`, `pydantic>=2.0` | OpenAI-compatible API server (`jarvis serve`). |
+
+### Other Extras
+
+| Extra | Install Command | Dependencies | Description |
+|-------|----------------|--------------|-------------|
+| `agents` | `pip install 'openjarvis[agents]'` | None | Agent infrastructure (included in base). |
+| `learning` | `pip install 'openjarvis[learning]'` | None | Learning/router policy system (included in base). |
+| `openclaw` | `pip install 'openjarvis[openclaw]'` | None | OpenClaw agent transport layer. Requires Node.js 22+ at runtime. |
+| `docs` | `pip install 'openjarvis[docs]'` | `mkdocs>=1.6`, `mkdocs-material>=9.5`, `mkdocstrings[python]>=0.25` | Documentation build tools. |
+| `dev` | `pip install 'openjarvis[dev]'` | `pytest>=8`, `pytest-asyncio>=0.24`, `pytest-cov>=5`, `respx>=0.22`, `ruff>=0.4` | Development and testing tools. |
+
+### Installing Multiple Extras
+
+Combine extras with commas:
+
+```bash
+pip install 'openjarvis[server,memory-faiss,inference-cloud]'
+```
+
+Or with `uv`:
+
+```bash
+uv pip install 'openjarvis[server,memory-faiss,inference-cloud]'
+```
+
+## Verifying Installation
+
+After installation, verify that the CLI is available:
+
+```bash
+jarvis --version
+```
+
+Expected output:
+
+```
+jarvis, version 1.0.0
+```
+
+View all available commands:
+
+```bash
+jarvis --help
+```
+
+Expected output:
+
+```
+Usage: jarvis [OPTIONS] COMMAND [ARGS]...
+
+ OpenJarvis -- modular AI assistant backend
+
+Options:
+ --version Show the version and exit.
+ --help Show this message and exit.
+
+Commands:
+ ask Ask Jarvis a question.
+ bench Run inference benchmarks.
+ init Detect hardware and generate ~/.openjarvis/config.toml.
+ memory Manage the memory store.
+ model Manage language models.
+ serve Start the OpenAI-compatible API server.
+ telemetry Query and manage inference telemetry data.
+```
+
+## Setting Up an Inference Backend
+
+OpenJarvis requires at least one inference backend to generate responses. Choose the backend that best matches your hardware.
+
+### Ollama (Recommended for most users)
+
+Ollama is the easiest way to get started. It handles model downloading and serving automatically.
+
+1. Install Ollama from [ollama.com](https://ollama.com)
+2. Start the server:
+
+ ```bash
+ ollama serve
+ ```
+
+3. Pull a model:
+
+ ```bash
+ ollama pull qwen3:8b
+ ```
+
+ Or pull directly via the Jarvis CLI:
+
+ ```bash
+ jarvis model pull qwen3:8b
+ ```
+
+4. Verify the engine is detected:
+
+ ```bash
+ jarvis model list
+ ```
+
+!!! tip "Best for: Apple Silicon Macs, consumer NVIDIA GPUs, CPU-only systems"
+
+### vLLM (High-throughput serving)
+
+vLLM provides high-throughput serving optimized for datacenter GPUs.
+
+1. Install vLLM following the [official guide](https://docs.vllm.ai)
+2. Start the server:
+
+ ```bash
+ vllm serve Qwen/Qwen2.5-7B-Instruct
+ ```
+
+3. OpenJarvis will auto-detect it at `http://localhost:8000`
+
+!!! tip "Best for: NVIDIA datacenter GPUs (A100, H100, L40), AMD GPUs"
+
+### llama.cpp (Lightweight, CPU-friendly)
+
+llama.cpp provides efficient CPU and GPU inference with GGUF quantized models.
+
+1. Build llama.cpp from [github.com/ggerganov/llama.cpp](https://github.com/ggerganov/llama.cpp)
+2. Start the server:
+
+ ```bash
+ llama-server -m /path/to/model.gguf --port 8080
+ ```
+
+3. OpenJarvis will auto-detect it at `http://localhost:8080`
+
+!!! tip "Best for: CPU-only machines, constrained environments, GGUF models"
+
+### SGLang
+
+SGLang provides structured generation and high-performance serving.
+
+1. Install SGLang following the [official guide](https://github.com/sgl-project/sglang)
+2. Start the server:
+
+ ```bash
+ python -m sglang.launch_server --model Qwen/Qwen2.5-7B-Instruct --port 30000
+ ```
+
+3. OpenJarvis will auto-detect it at `http://localhost:30000`
+
+### Cloud APIs (OpenAI, Anthropic, Google)
+
+For cloud-based inference, install the cloud extras and set your API keys:
+
+```bash
+pip install 'openjarvis[inference-cloud,inference-google]'
+```
+
+Set environment variables:
+
+```bash
+export OPENAI_API_KEY="sk-..."
+export ANTHROPIC_API_KEY="sk-ant-..."
+export GOOGLE_API_KEY="..."
+```
+
+OpenJarvis will automatically detect available cloud providers.
+
+## Next Steps
+
+- [Quick Start](quickstart.md) — Run your first query
+- [Configuration](configuration.md) — Customize engine hosts, model routing, memory, and more
diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md
new file mode 100644
index 00000000..0e4e4e62
--- /dev/null
+++ b/docs/getting-started/quickstart.md
@@ -0,0 +1,444 @@
+---
+title: Quick Start
+description: Get up and running with OpenJarvis in minutes
+---
+
+# Quick Start
+
+This guide walks through the core workflows of OpenJarvis: asking questions, using agents with tools, managing memory, running benchmarks, and starting the API server.
+
+!!! info "Prerequisites"
+ Make sure you have [installed OpenJarvis](installation.md) and have at least one inference backend running (e.g., `ollama serve`).
+
+## Initialize Configuration
+
+Start by detecting your hardware and generating a configuration file:
+
+```bash
+jarvis init
+```
+
+This runs hardware auto-detection (GPU vendor, VRAM, CPU, RAM) and writes a config file to `~/.openjarvis/config.toml` with sensible defaults for your system. It also selects the recommended inference engine.
+
+```
+Detecting hardware...
+ Platform : linux
+ CPU : AMD EPYC 7763 (128 cores)
+ RAM : 512.0 GB
+ GPU : NVIDIA A100 (80.0 GB VRAM, x8)
+
+Config written successfully.
+```
+
+To overwrite an existing config:
+
+```bash
+jarvis init --force
+```
+
+See [Configuration](configuration.md) for the full config reference.
+
+## Your First Question
+
+### Via CLI
+
+The simplest way to interact with OpenJarvis is the `ask` command:
+
+```bash
+jarvis ask "What is the capital of France?"
+```
+
+OpenJarvis will auto-detect a running engine, select a model using the configured router policy, and return the response.
+
+#### CLI Options
+
+| Option | Description | Example |
+|--------|-------------|---------|
+| `-m`, `--model` | Override model selection | `jarvis ask -m qwen3:8b "Hello"` |
+| `-e`, `--engine` | Force a specific engine | `jarvis ask -e ollama "Hello"` |
+| `-t`, `--temperature` | Sampling temperature (default: 0.7) | `jarvis ask -t 0.2 "Hello"` |
+| `--max-tokens` | Max tokens to generate (default: 1024) | `jarvis ask --max-tokens 2048 "Hello"` |
+| `--json` | Output raw JSON result | `jarvis ask --json "Hello"` |
+| `--no-stream` | Disable streaming | `jarvis ask --no-stream "Hello"` |
+| `--no-context` | Disable memory context injection | `jarvis ask --no-context "Hello"` |
+| `-a`, `--agent` | Use an agent | `jarvis ask -a orchestrator "Hello"` |
+| `--tools` | Comma-separated tools | `jarvis ask --tools calculator,think "2+2"` |
+| `--router` | Router policy for model selection | `jarvis ask --router heuristic "Hello"` |
+
+### Via Python SDK
+
+The `Jarvis` class provides a high-level Python interface:
+
+```python
+from openjarvis import Jarvis
+
+j = Jarvis()
+response = j.ask("What is the capital of France?")
+print(response)
+j.close()
+```
+
+For detailed results including token usage and model info:
+
+```python
+result = j.ask_full("What is the capital of France?")
+print(result["content"]) # The response text
+print(result["model"]) # Model that handled the query
+print(result["engine"]) # Engine that ran inference
+print(result["usage"]) # Token usage statistics
+```
+
+#### SDK Constructor Options
+
+```python
+# Use default config (auto-detected hardware, ~/.openjarvis/config.toml)
+j = Jarvis()
+
+# Override the model
+j = Jarvis(model="qwen3:8b")
+
+# Override the engine
+j = Jarvis(engine_key="ollama")
+
+# Use a custom config file
+j = Jarvis(config_path="/path/to/config.toml")
+```
+
+!!! warning "Always call `close()`"
+ The `Jarvis` instance holds references to telemetry stores and memory backends. Call `j.close()` when you are done to release resources.
+
+## Using Agents with Tools
+
+Agents add multi-turn reasoning and tool-calling capabilities. The `orchestrator` agent runs a tool-calling loop, invoking tools as needed to answer the query.
+
+### Available Agents
+
+| Agent | Description |
+|-------|-------------|
+| `simple` | Single-turn, no tools. Sends the query directly to the model. |
+| `orchestrator` | Multi-turn tool-calling loop. Invokes tools iteratively until it has an answer. |
+| `custom` | Template for user-defined agent logic. |
+| `openclaw` | HTTP/subprocess transport to external OpenClaw agent processes. |
+
+### Available Built-in Tools
+
+| Tool | Description |
+|------|-------------|
+| `calculator` | Safe mathematical expression evaluation (ast-based). |
+| `think` | Reasoning scratchpad for chain-of-thought. |
+| `retrieval` | Search the memory store for relevant context. |
+| `llm` | Make sub-queries to another model. |
+| `file_read` | Read files with path validation. |
+| `web_search` | Web search via the Tavily API (requires `tools-search` extra). |
+
+### CLI Example
+
+```bash
+jarvis ask --agent orchestrator --tools calculator,think "What is 137 * 42?"
+```
+
+### SDK Example
+
+```python
+from openjarvis import Jarvis
+
+j = Jarvis()
+result = j.ask_full(
+ "What is the square root of 144?",
+ agent="orchestrator",
+ tools=["calculator", "think"],
+)
+print(result["content"])
+print(result["tool_results"]) # List of tool invocations and results
+print(result["turns"]) # Number of agent turns
+j.close()
+```
+
+## Memory: Indexing and Search
+
+The memory system lets you index documents and inject relevant context into queries automatically.
+
+### Index Documents
+
+Index a file or directory. OpenJarvis chunks the content and stores it in the configured memory backend (SQLite/FTS5 by default).
+
+=== "CLI"
+
+ ```bash
+ # Index a directory
+ jarvis memory index ./docs/
+
+ # Index a single file with custom chunk size
+ jarvis memory index ./paper.txt --chunk-size 256 --chunk-overlap 32
+ ```
+
+=== "Python SDK"
+
+ ```python
+ from openjarvis import Jarvis
+
+ j = Jarvis()
+ result = j.memory.index("./docs/", chunk_size=512, chunk_overlap=64)
+ print(f"Indexed {result['chunks']} chunks")
+ j.close()
+ ```
+
+### Search Memory
+
+Query the memory store to find relevant chunks:
+
+=== "CLI"
+
+ ```bash
+ jarvis memory search "configuration options"
+ jarvis memory search -k 10 "how to deploy"
+ ```
+
+=== "Python SDK"
+
+ ```python
+ results = j.memory.search("configuration options", top_k=5)
+ for r in results:
+ print(f"[{r['score']:.4f}] {r['source']}: {r['content'][:100]}")
+ ```
+
+### Check Memory Statistics
+
+=== "CLI"
+
+ ```bash
+ jarvis memory stats
+ ```
+
+=== "Python SDK"
+
+ ```python
+ stats = j.memory.stats()
+ print(f"Backend: {stats['backend']}, Documents: {stats.get('count', 'N/A')}")
+ ```
+
+### Automatic Context Injection
+
+When you have indexed documents, OpenJarvis automatically injects relevant context into your queries. The memory system searches for chunks matching your query and prepends them as system context before sending to the model.
+
+To disable this behavior:
+
+=== "CLI"
+
+ ```bash
+ jarvis ask --no-context "Hello"
+ ```
+
+=== "Python SDK"
+
+ ```python
+ response = j.ask("Hello", context=False)
+ ```
+
+Context injection is controlled by the `[memory]` config section. See [Configuration](configuration.md) for details on `context_injection`, `context_top_k`, `context_min_score`, and `context_max_tokens`.
+
+## Model Management
+
+### List Available Models
+
+See all models available on running engines:
+
+```bash
+jarvis model list
+```
+
+This produces a table showing each model, its engine, parameter count, context length, and VRAM requirements.
+
+### Get Model Details
+
+```bash
+jarvis model info qwen3:8b
+```
+
+### Pull a Model (Ollama)
+
+```bash
+jarvis model pull qwen3:8b
+```
+
+### SDK Model Listing
+
+```python
+from openjarvis import Jarvis
+
+j = Jarvis()
+models = j.list_models()
+engines = j.list_engines()
+print(f"Models: {models}")
+print(f"Engines: {engines}")
+j.close()
+```
+
+## Running Benchmarks
+
+The benchmarking framework measures inference latency and throughput against your engine.
+
+=== "All benchmarks"
+
+ ```bash
+ jarvis bench run
+ ```
+
+=== "Specific benchmark"
+
+ ```bash
+ jarvis bench run -b latency
+ jarvis bench run -b throughput
+ ```
+
+=== "Custom options"
+
+ ```bash
+ # 20 samples, JSON output
+ jarvis bench run -n 20 --json
+
+ # Specific model and engine, write to file
+ jarvis bench run -m qwen3:8b -e ollama -o results.jsonl
+ ```
+
+Example output:
+
+```
+Running 2 benchmark(s) on ollama/qwen3:8b (10 samples)...
+
+latency (10 samples, 0 errors)
+ mean_ms: 245.3200
+ p50_ms: 238.1000
+ p95_ms: 312.4500
+ min_ms: 201.2000
+ max_ms: 345.6000
+
+throughput (10 samples, 0 errors)
+ tokens_per_second: 42.1500
+ total_tokens: 4215
+ total_seconds: 100.0000
+```
+
+## Starting the API Server
+
+OpenJarvis provides an OpenAI-compatible API server for integration with existing tools and frontends.
+
+!!! note "Requires the `server` extra"
+ ```bash
+ pip install 'openjarvis[server]'
+ ```
+
+### Start the Server
+
+```bash
+jarvis serve --port 8000
+```
+
+With custom options:
+
+```bash
+jarvis serve --host 0.0.0.0 --port 8000 --engine ollama --model qwen3:8b --agent orchestrator
+```
+
+### API Endpoints
+
+| Endpoint | Method | Description |
+|----------|--------|-------------|
+| `/v1/chat/completions` | `POST` | Chat completions (streaming and non-streaming) |
+| `/v1/models` | `GET` | List available models |
+| `/health` | `GET` | Health check |
+
+### Use with Any OpenAI-Compatible Client
+
+Once the server is running, point any OpenAI-compatible client at it:
+
+```python
+from openai import OpenAI
+
+client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
+response = client.chat.completions.create(
+ model="qwen3:8b",
+ messages=[{"role": "user", "content": "Hello!"}],
+)
+print(response.choices[0].message.content)
+```
+
+Or with `curl`:
+
+```bash
+curl http://localhost:8000/v1/chat/completions \
+ -H "Content-Type: application/json" \
+ -d '{
+ "model": "qwen3:8b",
+ "messages": [{"role": "user", "content": "Hello!"}]
+ }'
+```
+
+## Telemetry
+
+OpenJarvis records telemetry for every inference call (timing, tokens, cost). View aggregated statistics:
+
+```bash
+jarvis telemetry stats
+```
+
+Export telemetry data:
+
+```bash
+jarvis telemetry export --format json
+jarvis telemetry export --format csv -o telemetry.csv
+```
+
+Clear all telemetry records:
+
+```bash
+jarvis telemetry clear --yes
+```
+
+## Complete Working Example
+
+Here is a complete end-to-end session combining multiple features:
+
+```python
+from openjarvis import Jarvis
+
+# Initialize with defaults (auto-detect hardware and engine)
+j = Jarvis()
+
+# 1. Index some documentation
+index_result = j.memory.index("./docs/", chunk_size=512)
+print(f"Indexed {index_result['chunks']} chunks from {index_result['path']}")
+
+# 2. Search memory
+results = j.memory.search("how to configure engines")
+for r in results:
+ print(f" [{r['score']:.3f}] {r['source']}")
+
+# 3. Ask a question (memory context is injected automatically)
+answer = j.ask("How do I configure the Ollama engine host?")
+print(f"\nAnswer: {answer}")
+
+# 4. Use an agent with tools
+calc_result = j.ask_full(
+ "Calculate the compound interest on $10,000 at 5% for 10 years",
+ agent="orchestrator",
+ tools=["calculator", "think"],
+)
+print(f"\nCalculation: {calc_result['content']}")
+print(f"Tools used: {[t['tool_name'] for t in calc_result['tool_results']]}")
+print(f"Agent turns: {calc_result['turns']}")
+
+# 5. List available models
+models = j.list_models()
+print(f"\nAvailable models: {models}")
+
+# 6. Clean up
+j.close()
+```
+
+## Next Steps
+
+- [Configuration](configuration.md) — Fine-tune engine hosts, model routing, memory settings, and more
+- [CLI Reference](../user-guide/cli.md) — Full reference for all CLI commands and options
+- [Python SDK](../user-guide/python-sdk.md) — Detailed SDK documentation
+- [Architecture Overview](../architecture/overview.md) — Understand the four-pillar design
diff --git a/docs/includes/abbreviations.md b/docs/includes/abbreviations.md
new file mode 100644
index 00000000..156731a1
--- /dev/null
+++ b/docs/includes/abbreviations.md
@@ -0,0 +1,13 @@
+*[ABC]: Abstract Base Class
+*[FTS5]: Full-Text Search version 5
+*[GRPO]: Group Relative Policy Optimization
+*[RRF]: Reciprocal Rank Fusion
+*[SSE]: Server-Sent Events
+*[VRAM]: Video Random Access Memory
+*[MoE]: Mixture of Experts
+*[GGUF]: GPT-Generated Unified Format
+*[MCP]: Model Context Protocol
+*[SDK]: Software Development Kit
+*[CLI]: Command-Line Interface
+*[API]: Application Programming Interface
+*[LLM]: Large Language Model
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..08f036c4
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,184 @@
+---
+title: OpenJarvis
+description: Programming abstractions for on-device AI
+---
+
+# OpenJarvis
+
+**Programming abstractions for on-device AI.**
+
+OpenJarvis is a modular framework for building, running, and learning from local AI systems. It provides composable abstractions across four core pillars — Intelligence, Engine, Agentic Logic, and Memory — with a cross-cutting trace-driven learning system that improves routing decisions over time.
+
+Everything runs on your hardware. Cloud APIs are optional.
+
+---
+
+## Key Features
+
+
+
+- **Four Core Pillars**
+
+ ---
+
+ Intelligence (model routing), Engine (inference runtime), Agentic Logic (tool-calling agents), and Memory (persistent searchable storage) — each with a clear ABC interface and decorator-based registry.
+
+- **5 Engine Backends**
+
+ ---
+
+ Ollama, vLLM, SGLang, llama.cpp, and cloud (OpenAI/Anthropic/Google). All implement the same `InferenceEngine` ABC with `generate()`, `stream()`, `list_models()`, and `health()`.
+
+- **5 Memory Backends**
+
+ ---
+
+ SQLite/FTS5 (default, zero-dependency), FAISS, ColBERTv2, BM25, and Hybrid (reciprocal rank fusion). Document chunking, indexing, and context injection built in.
+
+- **Hardware-Aware**
+
+ ---
+
+ Auto-detects GPU vendor, model, and VRAM via `nvidia-smi`, `rocm-smi`, and `system_profiler`. Recommends the optimal engine for your hardware automatically.
+
+- **Offline-First**
+
+ ---
+
+ All core functionality works without a network connection. Cloud API backends are optional extras for when you need them.
+
+- **OpenAI-Compatible API**
+
+ ---
+
+ `jarvis serve` starts a FastAPI server with `POST /v1/chat/completions`, `GET /v1/models`, and SSE streaming. Drop-in replacement for OpenAI-compatible clients.
+
+- **Trace-Driven Learning**
+
+ ---
+
+ Every agent interaction is recorded as a trace. The learning system uses accumulated traces to improve model routing decisions. Pluggable router policies: heuristic, trace-driven, and GRPO.
+
+- **Python SDK**
+
+ ---
+
+ The `Jarvis` class provides a high-level sync API. Three lines of code to ask a question. Full access to agents, tools, memory, and model routing.
+
+- **CLI-First**
+
+ ---
+
+ `jarvis ask`, `jarvis serve`, `jarvis memory`, `jarvis bench`, `jarvis telemetry` — every capability is accessible from the command line with rich terminal output.
+
+
+
+---
+
+## Quick Start
+
+### Python SDK
+
+```python
+from openjarvis import Jarvis
+
+j = Jarvis()
+response = j.ask("Explain quicksort in two sentences.")
+print(response)
+j.close()
+```
+
+For more control, use `ask_full()` to get usage stats, model info, and tool results:
+
+```python
+result = j.ask_full(
+ "What is 2 + 2?",
+ agent="orchestrator",
+ tools=["calculator"],
+)
+print(result["content"]) # "4"
+print(result["tool_results"]) # [{tool_name: "calculator", ...}]
+```
+
+### CLI
+
+```bash
+# Ask a question
+jarvis ask "What is the capital of France?"
+
+# Use an agent with tools
+jarvis ask --agent orchestrator --tools calculator,think "What is 137 * 42?"
+
+# Start the API server
+jarvis serve --port 8000
+
+# Index documents and search memory
+jarvis memory index ./docs/
+jarvis memory search "configuration options"
+
+# Run inference benchmarks
+jarvis bench run --json
+```
+
+---
+
+## Project Status
+
+OpenJarvis v1.0 is complete. The framework includes the full four-pillar architecture, Python SDK, CLI, OpenAI-compatible API server, OpenClaw agent infrastructure, benchmarking framework, and Docker deployment. The test suite contains over 1,000 tests. Phase 6 (trace system and trace-driven learning) is in active development.
+
+| Component | Status |
+|-----------|--------|
+| Intelligence (model routing) | Stable |
+| Engine (5 backends) | Stable |
+| Agentic Logic (agents + tools) | Stable |
+| Memory (5 backends) | Stable |
+| Python SDK | Stable |
+| CLI | Stable |
+| API Server | Stable |
+| Trace System | Active Development |
+| Trace-Driven Learning | Active Development |
+| Docker Deployment | Stable |
+
+---
+
+## Documentation
+
+
+
+- **[Getting Started](getting-started/installation.md)**
+
+ ---
+
+ Install OpenJarvis, configure your first engine, and run your first query in minutes.
+
+- **[User Guide](user-guide/cli.md)**
+
+ ---
+
+ Comprehensive guides for the CLI, Python SDK, agents, memory, tools, telemetry, and benchmarks.
+
+- **[Architecture](architecture/overview.md)**
+
+ ---
+
+ Deep dive into the four-pillar design, registry pattern, query flow, and cross-cutting learning system.
+
+- **[API Reference](api/index.md)**
+
+ ---
+
+ Auto-generated reference for every module: SDK, core, engine, agents, memory, tools, intelligence, learning, traces, telemetry, and server.
+
+- **[Deployment](deployment/docker.md)**
+
+ ---
+
+ Deploy OpenJarvis with Docker, systemd, or launchd. Includes GPU-accelerated container images.
+
+- **[Development](development/contributing.md)**
+
+ ---
+
+ Contributing guide, extension patterns, roadmap, and changelog.
+
+
diff --git a/docs/user-guide/agents.md b/docs/user-guide/agents.md
new file mode 100644
index 00000000..f6f1355d
--- /dev/null
+++ b/docs/user-guide/agents.md
@@ -0,0 +1,269 @@
+# Agents
+
+Agents are the agentic logic layer of OpenJarvis. They determine how a query is processed -- whether it goes directly to a model, through a tool-calling loop, or via an external agent runtime. All agents implement the `BaseAgent` ABC and are registered via the `AgentRegistry`.
+
+## Overview
+
+| Agent | Registry Key | Tools | Multi-turn | Description |
+|------------------|-----------------|-------|------------|----------------------------------------------|
+| `SimpleAgent` | `simple` | No | No | Single-turn query-to-response |
+| `OrchestratorAgent` | `orchestrator` | Yes | Yes | Multi-turn tool-calling loop |
+| `OpenClawAgent` | `openclaw` | Yes | Yes | External agent via HTTP or subprocess |
+| `CustomAgent` | `custom` | -- | -- | Template for user-defined agents |
+
+---
+
+## BaseAgent ABC
+
+All agents extend the abstract `BaseAgent` class.
+
+```python
+from abc import ABC, abstractmethod
+from openjarvis.agents._stubs import AgentContext, AgentResult
+
+class BaseAgent(ABC):
+ agent_id: str
+
+ @abstractmethod
+ def run(
+ self,
+ input: str,
+ context: AgentContext | None = None,
+ **kwargs,
+ ) -> AgentResult:
+ """Execute the agent on the given input."""
+```
+
+### AgentContext
+
+The runtime context handed to an agent on each invocation.
+
+| Field | Type | Description |
+|------------------|--------------------|------------------------------------------------|
+| `conversation` | `Conversation` | Message history (pre-filled with context if memory injection is active) |
+| `tools` | `list[str]` | Tool names available to the agent |
+| `memory_results` | `list[Any]` | Pre-fetched memory retrieval results |
+| `metadata` | `dict[str, Any]` | Arbitrary metadata for the run |
+
+### AgentResult
+
+The result returned after an agent completes a run.
+
+| Field | Type | Description |
+|----------------|--------------------|------------------------------------------------|
+| `content` | `str` | The final response text |
+| `tool_results` | `list[ToolResult]` | Results from tool executions during the run |
+| `turns` | `int` | Number of turns (inference calls) taken |
+| `metadata` | `dict[str, Any]` | Arbitrary metadata about the run |
+
+---
+
+## SimpleAgent
+
+The `SimpleAgent` is a single-turn agent that sends the query directly to the inference engine and returns the response. It does not support tool calling.
+
+**How it works:**
+
+1. Builds a message list from the conversation context (if provided) plus the user query.
+2. Calls the inference engine via `instrumented_generate()` for telemetry tracking.
+3. Returns the response as an `AgentResult` with `turns=1`.
+
+**Constructor parameters:**
+
+| Parameter | Type | Default | Description |
+|---------------|-------------------|---------|------------------------------------|
+| `engine` | `InferenceEngine` | -- | The inference engine to use |
+| `model` | `str` | -- | Model identifier |
+| `bus` | `EventBus` | `None` | Event bus for telemetry |
+| `temperature` | `float` | `0.7` | Sampling temperature |
+| `max_tokens` | `int` | `1024` | Maximum tokens to generate |
+
+**When to use:** For straightforward question-answering without tool calling or multi-turn reasoning.
+
+---
+
+## OrchestratorAgent
+
+The `OrchestratorAgent` is a multi-turn agent that implements a tool-calling loop. It is the primary agent for queries that require computation, knowledge retrieval, or structured reasoning.
+
+**How it works:**
+
+1. Builds the initial message list from context and the user query.
+2. Sends messages with tool definitions (OpenAI function-calling format) to the engine.
+3. If the engine responds with `tool_calls`, the `ToolExecutor` dispatches each call.
+4. Tool results are appended as `TOOL` messages and the loop continues.
+5. If no `tool_calls` are returned, the response is treated as the final answer.
+6. The loop stops after `max_turns` iterations (default: 10), returning whatever content is available along with a `max_turns_exceeded` metadata flag.
+
+**Constructor parameters:**
+
+| Parameter | Type | Default | Description |
+|---------------|-------------------|---------|------------------------------------|
+| `engine` | `InferenceEngine` | -- | The inference engine to use |
+| `model` | `str` | -- | Model identifier |
+| `tools` | `list[BaseTool]` | `[]` | Tool instances to make available |
+| `bus` | `EventBus` | `None` | Event bus for telemetry |
+| `max_turns` | `int` | `10` | Maximum number of tool-calling turns |
+| `temperature` | `float` | `0.7` | Sampling temperature |
+| `max_tokens` | `int` | `1024` | Maximum tokens to generate |
+
+**When to use:** For queries that need calculation, memory search, sub-model calls, file reading, or multi-step reasoning.
+
+!!! info "Tool-Calling Loop"
+ The orchestrator follows the OpenAI function-calling convention. The engine must support returning `tool_calls` in its response for the loop to engage. If tools are provided but the engine does not return any tool calls, the agent behaves like a single-turn agent.
+
+---
+
+## OpenClawAgent
+
+The `OpenClawAgent` wraps the OpenClaw Pi agent runtime, communicating via either HTTP or subprocess transport. It supports tool calling through the OpenClaw protocol.
+
+**How it works:**
+
+1. Checks transport health.
+2. Sends a `QUERY` protocol message through the transport.
+3. If the response is a `TOOL_CALL`, dispatches the tool locally via `ToolExecutor`.
+4. Sends the tool result back as a `TOOL_RESULT` message.
+5. Continues the tool-call loop until the response is a final answer or error (up to 10 turns).
+
+**Constructor parameters:**
+
+| Parameter | Type | Default | Description |
+|--------------|----------------------|-----------|-------------------------------------------|
+| `engine` | `Any` | `None` | Inference engine (fallback/provider) |
+| `model` | `str` | `""` | Model identifier |
+| `transport` | `OpenClawTransport` | `None` | Pre-configured transport (overrides mode) |
+| `mode` | `str` | `"http"` | Transport mode: `"http"` or `"subprocess"` |
+| `bus` | `EventBus` | `None` | Event bus for telemetry |
+
+**Transport modes:**
+
+- **HTTP** (`HttpTransport`): Sends HTTP POST requests to an OpenClaw server.
+- **Subprocess** (`SubprocessTransport`): Spawns a Node.js process and communicates via stdin/stdout using JSON-line protocol.
+
+!!! warning "Node.js Requirement"
+ The subprocess transport mode requires Node.js 22+ to be installed on the system.
+
+---
+
+## CustomAgent
+
+The `CustomAgent` is a template for building user-defined agents. It raises `NotImplementedError` by default -- subclass it and override `run()` to implement your logic.
+
+```python
+from openjarvis.agents._stubs import AgentContext, AgentResult, BaseAgent
+from openjarvis.core.registry import AgentRegistry
+
+
+@AgentRegistry.register("my-agent")
+class MyAgent(BaseAgent):
+ agent_id = "my-agent"
+
+ def __init__(self, engine, model, **kwargs):
+ self._engine = engine
+ self._model = model
+
+ def run(self, input: str, context: AgentContext | None = None, **kwargs) -> AgentResult:
+ # Your custom logic here
+ result = self._engine.generate(
+ [{"role": "user", "content": input}],
+ model=self._model,
+ )
+ return AgentResult(
+ content=result.get("content", ""),
+ turns=1,
+ )
+```
+
+After registration, you can use your custom agent via the CLI or SDK:
+
+```bash
+jarvis ask --agent my-agent "Hello"
+```
+
+```python
+response = j.ask("Hello", agent="my-agent")
+```
+
+---
+
+## Using Agents
+
+### Via CLI
+
+```bash
+# Simple agent
+jarvis ask --agent simple "What is the capital of France?"
+
+# Orchestrator with tools
+jarvis ask --agent orchestrator --tools calculator,think "What is sqrt(256)?"
+
+# OpenClaw agent
+jarvis ask --agent openclaw "Tell me a story"
+```
+
+### Via Python SDK
+
+```python
+from openjarvis import Jarvis
+
+j = Jarvis()
+
+# Simple agent
+response = j.ask("Hello", agent="simple")
+
+# Orchestrator with tools
+response = j.ask(
+ "Calculate 15% of 340",
+ agent="orchestrator",
+ tools=["calculator"],
+)
+
+# Full result with tool details
+result = j.ask_full(
+ "What is the square root of 144?",
+ agent="orchestrator",
+ tools=["calculator", "think"],
+)
+print(result["content"])
+print(result["turns"])
+print(result["tool_results"])
+
+j.close()
+```
+
+---
+
+## Agent Registration
+
+Agents are registered via the `@AgentRegistry.register()` decorator. This makes them discoverable by name at runtime:
+
+```python
+from openjarvis.core.registry import AgentRegistry
+
+# Check if an agent is registered
+AgentRegistry.contains("orchestrator") # True
+
+# Get the agent class
+agent_cls = AgentRegistry.get("orchestrator")
+
+# List all registered agent keys
+AgentRegistry.keys() # ["simple", "orchestrator", "openclaw", "custom"]
+```
+
+---
+
+## Event Bus Integration
+
+All agents publish events on the `EventBus` when a bus is provided:
+
+| Event | When |
+|-------------------------|---------------------------------------------|
+| `AGENT_TURN_START` | At the beginning of a run |
+| `AGENT_TURN_END` | At the end of a run (includes turn count) |
+| `INFERENCE_START` | Before each engine call (orchestrator) |
+| `INFERENCE_END` | After each engine call (orchestrator) |
+| `TOOL_CALL_START` | Before each tool execution (openclaw) |
+| `TOOL_CALL_END` | After each tool execution (openclaw) |
+
+These events enable the telemetry and trace systems to record detailed interaction data automatically.
diff --git a/docs/user-guide/benchmarks.md b/docs/user-guide/benchmarks.md
new file mode 100644
index 00000000..e0ddea53
--- /dev/null
+++ b/docs/user-guide/benchmarks.md
@@ -0,0 +1,337 @@
+# Benchmarks
+
+The benchmarking framework measures inference engine performance with reproducible, standardized tests. It includes built-in benchmarks for latency and throughput, a suite runner for batch execution, and support for custom benchmarks.
+
+## Overview
+
+OpenJarvis ships with two benchmarks:
+
+| Benchmark | Registry Key | Measures |
+|---------------|----------------|-----------------------------------------------|
+| **Latency** | `latency` | Per-call inference latency (mean, p50, p95, min, max) |
+| **Throughput**| `throughput` | Tokens per second throughput |
+
+---
+
+## BaseBenchmark ABC
+
+All benchmarks implement the `BaseBenchmark` abstract base class.
+
+```python
+from abc import ABC, abstractmethod
+from openjarvis.bench._stubs import BenchmarkResult
+from openjarvis.engine._stubs import InferenceEngine
+
+class BaseBenchmark(ABC):
+
+ @property
+ @abstractmethod
+ def name(self) -> str:
+ """Short identifier for this benchmark."""
+
+ @property
+ @abstractmethod
+ def description(self) -> str:
+ """Human-readable description of what this benchmark measures."""
+
+ @abstractmethod
+ def run(
+ self,
+ engine: InferenceEngine,
+ model: str,
+ *,
+ num_samples: int = 10,
+ ) -> BenchmarkResult:
+ """Execute the benchmark and return results."""
+```
+
+### BenchmarkResult
+
+Each benchmark run produces a `BenchmarkResult`:
+
+| Field | Type | Description |
+|------------------|------------------|------------------------------------------|
+| `benchmark_name` | `str` | Name of the benchmark |
+| `model` | `str` | Model used |
+| `engine` | `str` | Engine backend used |
+| `metrics` | `dict[str, float]` | Key-value pairs of measured metrics |
+| `metadata` | `dict[str, Any]` | Additional metadata |
+| `samples` | `int` | Number of samples run |
+| `errors` | `int` | Number of errors encountered |
+
+---
+
+## Built-in Benchmarks
+
+### Latency Benchmark
+
+Measures per-call inference latency using short, fixed prompts. Each sample sends a simple prompt to the engine and measures wall-clock time.
+
+**Prompts used:** The benchmark rotates through a set of short canned prompts ("Hello", "What is 2+2?", "Explain gravity in one sentence") to keep input variation consistent across runs.
+
+**Metrics produced:**
+
+| Metric | Description |
+|-----------------|-----------------------------------------------------|
+| `mean_latency` | Average latency across all successful samples |
+| `p50_latency` | Median latency (50th percentile) |
+| `p95_latency` | 95th percentile latency (tail performance) |
+| `min_latency` | Fastest single call |
+| `max_latency` | Slowest single call |
+
+**Example output:**
+
+```
+latency (10 samples, 0 errors)
+ mean_latency: 0.2345
+ p50_latency: 0.2100
+ p95_latency: 0.3800
+ min_latency: 0.1500
+ max_latency: 0.4200
+```
+
+### Throughput Benchmark
+
+Measures inference throughput in tokens per second. Each sample sends a longer prompt ("Write a short paragraph about artificial intelligence") and measures both the time taken and the number of completion tokens generated.
+
+**Metrics produced:**
+
+| Metric | Description |
+|-----------------------|------------------------------------------------|
+| `tokens_per_second` | Total completion tokens / total time |
+| `total_tokens` | Total completion tokens across all samples |
+| `total_time_seconds` | Total wall-clock time across all samples |
+
+**Example output:**
+
+```
+throughput (10 samples, 0 errors)
+ tokens_per_second: 45.6789
+ total_tokens: 1250.0000
+ total_time_seconds: 27.3600
+```
+
+---
+
+## Interpreting Results
+
+### Latency Metrics
+
+- **mean_latency:** The average response time. Use this for general performance comparison.
+- **p50_latency (median):** The typical response time. Less affected by outliers than the mean.
+- **p95_latency:** The worst-case response time for 95% of requests. Critical for user experience -- if this is too high, some users will experience noticeable delays.
+- **min/max_latency:** The best and worst individual calls. A large gap between min and max indicates inconsistent performance.
+
+!!! tip "What to look for"
+ A healthy setup has `p95 / p50 < 2`. If the p95 is much higher than the median, investigate whether the engine is experiencing contention, thermal throttling, or memory pressure.
+
+### Throughput Metrics
+
+- **tokens_per_second:** The main throughput indicator. Higher is better. Typical ranges:
+ - CPU-only: 5-20 tokens/second
+ - Consumer GPU (RTX 3060-4090): 30-100 tokens/second
+ - Data-center GPU (A100, H100): 100-500+ tokens/second
+- **total_tokens / total_time:** The raw data behind the throughput calculation. Useful for verifying that the engine is generating meaningful output (not returning empty responses).
+
+---
+
+## BenchmarkSuite
+
+The `BenchmarkSuite` class runs a collection of benchmarks and provides aggregation and serialization utilities.
+
+```python
+from openjarvis.bench._stubs import BenchmarkSuite
+from openjarvis.bench.latency import LatencyBenchmark
+from openjarvis.bench.throughput import ThroughputBenchmark
+
+suite = BenchmarkSuite([LatencyBenchmark(), ThroughputBenchmark()])
+
+# Run all benchmarks
+results = suite.run_all(engine, model, num_samples=20)
+
+# Serialize to JSONL (one JSON object per line)
+jsonl = suite.to_jsonl(results)
+
+# Get a summary dict
+summary = suite.summary(results)
+```
+
+### Methods
+
+| Method | Returns | Description |
+|-------------------------|--------------------|--------------------------------------------|
+| `run_all(engine, model, num_samples=10)` | `list[BenchmarkResult]` | Run all benchmarks sequentially |
+| `to_jsonl(results)` | `str` | Serialize results to JSONL format |
+| `summary(results)` | `dict[str, Any]` | Create a summary dictionary |
+
+### JSONL Format
+
+Each line in the JSONL output is a JSON object:
+
+```json
+{"benchmark_name": "latency", "model": "qwen3:8b", "engine": "ollama", "metrics": {"mean_latency": 0.234, "p50_latency": 0.21, "p95_latency": 0.38, "min_latency": 0.15, "max_latency": 0.42}, "metadata": {}, "samples": 10, "errors": 0}
+{"benchmark_name": "throughput", "model": "qwen3:8b", "engine": "ollama", "metrics": {"tokens_per_second": 45.67, "total_tokens": 1250.0, "total_time_seconds": 27.36}, "metadata": {}, "samples": 10, "errors": 0}
+```
+
+### Summary Format
+
+```json
+{
+ "benchmark_count": 2,
+ "benchmarks": [
+ {
+ "name": "latency",
+ "model": "qwen3:8b",
+ "engine": "ollama",
+ "metrics": {"mean_latency": 0.234, ...},
+ "samples": 10,
+ "errors": 0
+ },
+ {
+ "name": "throughput",
+ "model": "qwen3:8b",
+ "engine": "ollama",
+ "metrics": {"tokens_per_second": 45.67, ...},
+ "samples": 10,
+ "errors": 0
+ }
+ ]
+}
+```
+
+---
+
+## CLI Usage
+
+```bash
+# Run all benchmarks with default settings (10 samples)
+jarvis bench run
+
+# Run with more samples for better statistical accuracy
+jarvis bench run -n 50
+
+# Run only the latency benchmark
+jarvis bench run -b latency
+
+# Run only the throughput benchmark with 20 samples
+jarvis bench run -b throughput -n 20
+
+# Specify model and engine
+jarvis bench run -m qwen3:8b -e ollama
+
+# Output JSON summary to stdout
+jarvis bench run --json
+
+# Write JSONL results to a file
+jarvis bench run -o results.jsonl
+
+# Combine options
+jarvis bench run -b latency -n 100 -m qwen3:8b --json -o latency.jsonl
+```
+
+| Option | Type | Default | Description |
+|----------------------------|--------|---------|------------------------------------------|
+| `-m`, `--model MODEL` | string | auto | Model to benchmark |
+| `-e`, `--engine ENGINE` | string | auto | Engine backend |
+| `-n`, `--samples N` | int | `10` | Number of samples per benchmark |
+| `-b`, `--benchmark NAME` | string | all | Specific benchmark to run (`latency` or `throughput`) |
+| `-o`, `--output PATH` | path | none | Write JSONL results to file |
+| `--json` | flag | off | Output JSON summary to stdout |
+
+---
+
+## Adding Custom Benchmarks
+
+Create a custom benchmark by subclassing `BaseBenchmark` and registering it with the `BenchmarkRegistry`.
+
+### Step 1: Implement the Benchmark
+
+```python
+import time
+from openjarvis.bench._stubs import BaseBenchmark, BenchmarkResult
+from openjarvis.core.registry import BenchmarkRegistry
+from openjarvis.core.types import Message, Role
+from openjarvis.engine._stubs import InferenceEngine
+
+
+class ContextLengthBenchmark(BaseBenchmark):
+ """Measures how latency scales with input length."""
+
+ @property
+ def name(self) -> str:
+ return "context_length"
+
+ @property
+ def description(self) -> str:
+ return "Measures latency scaling with increasing input length"
+
+ def run(
+ self,
+ engine: InferenceEngine,
+ model: str,
+ *,
+ num_samples: int = 10,
+ ) -> BenchmarkResult:
+ latencies = {}
+ errors = 0
+
+ for length in [100, 500, 1000, 2000]:
+ prompt = "x " * length
+ messages = [Message(role=Role.USER, content=prompt)]
+
+ t0 = time.time()
+ try:
+ engine.generate(messages, model=model)
+ latencies[f"latency_{length}_tokens"] = time.time() - t0
+ except Exception:
+ errors += 1
+
+ return BenchmarkResult(
+ benchmark_name=self.name,
+ model=model,
+ engine=engine.engine_id,
+ metrics=latencies,
+ samples=len(latencies),
+ errors=errors,
+ )
+```
+
+### Step 2: Register the Benchmark
+
+Use the `ensure_registered()` pattern to survive registry clearing in tests:
+
+```python
+def ensure_registered() -> None:
+ """Register the benchmark if not already present."""
+ if not BenchmarkRegistry.contains("context_length"):
+ BenchmarkRegistry.register_value("context_length", ContextLengthBenchmark)
+```
+
+Alternatively, use the decorator at class definition time:
+
+```python
+@BenchmarkRegistry.register("context_length")
+class ContextLengthBenchmark(BaseBenchmark):
+ ...
+```
+
+!!! info "The `ensure_registered()` Pattern"
+ The `ensure_registered()` function is preferred over the decorator for benchmark modules because it survives registry clearing during testing. The built-in `latency` and `throughput` benchmarks both use this pattern. The benchmark CLI command calls `ensure_registered()` before looking up benchmarks.
+
+### Step 3: Use Your Benchmark
+
+Once registered, your benchmark is available through the CLI:
+
+```bash
+jarvis bench run -b context_length
+```
+
+And through the `BenchmarkSuite`:
+
+```python
+from openjarvis.core.registry import BenchmarkRegistry
+
+bench_cls = BenchmarkRegistry.get("context_length")
+bench = bench_cls()
+result = bench.run(engine, model, num_samples=5)
+```
diff --git a/docs/user-guide/cli.md b/docs/user-guide/cli.md
new file mode 100644
index 00000000..826a5ca3
--- /dev/null
+++ b/docs/user-guide/cli.md
@@ -0,0 +1,395 @@
+# CLI Reference
+
+OpenJarvis provides a command-line interface through the `jarvis` command. Built on [Click](https://click.palletsprojects.com/), it offers subcommands for querying models, managing memory, running benchmarks, and serving an OpenAI-compatible API.
+
+## Global Options
+
+```bash
+jarvis --version # Print the OpenJarvis version
+jarvis --help # Show top-level help with all subcommands
+```
+
+## `jarvis init`
+
+Detect local hardware (CPU, GPU, RAM) and generate a configuration file at `~/.openjarvis/config.toml`.
+
+```bash
+jarvis init # Interactive — refuses to overwrite existing config
+jarvis init --force # Overwrite existing config without prompting
+```
+
+| Option | Description |
+|-----------|-----------------------------------------------|
+| `--force` | Overwrite existing configuration without prompting |
+
+The `init` command auto-detects:
+
+- **Platform** (Linux, macOS, Windows)
+- **CPU** brand and core count
+- **RAM** in GB
+- **GPU** vendor, model, VRAM, and count (via `nvidia-smi`, `rocm-smi`, or `system_profiler`)
+
+Based on the detected hardware, it recommends an appropriate inference engine and writes a pre-configured TOML file.
+
+**Example output:**
+
+```
+Detecting hardware...
+ Platform : linux
+ CPU : AMD Ryzen 9 7950X (32 cores)
+ RAM : 64 GB
+ GPU : NVIDIA RTX 4090 (24.0 GB VRAM, x1)
+
+Config written successfully.
+```
+
+---
+
+## `jarvis ask`
+
+Send a query to the inference engine (directly or through an agent) and print the response.
+
+```bash
+jarvis ask "What is the capital of France?"
+```
+
+### Options
+
+| Option | Type | Default | Description |
+|-------------------------------|---------|------------|-------------------------------------------------------|
+| `-m`, `--model MODEL` | string | auto | Model to use for inference |
+| `-e`, `--engine ENGINE` | string | auto | Engine backend (ollama, vllm, llamacpp, etc.) |
+| `-t`, `--temperature TEMP` | float | `0.7` | Sampling temperature |
+| `--max-tokens N` | int | `1024` | Maximum tokens to generate |
+| `--json` | flag | off | Output raw JSON result instead of plain text |
+| `--no-stream` | flag | off | Disable streaming (synchronous mode) |
+| `--no-context` | flag | off | Disable memory context injection |
+| `-a`, `--agent AGENT` | string | none | Agent to use (`simple`, `orchestrator`) |
+| `--tools TOOLS` | string | none | Comma-separated tool names to enable |
+| `--router POLICY` | string | config | Router policy for model selection (`heuristic`, `grpo`)|
+
+### Direct Mode vs Agent Mode
+
+**Direct mode** (default) sends the query straight to the inference engine:
+
+```bash
+jarvis ask "Explain quantum computing"
+```
+
+**Agent mode** routes the query through an agent that can use tools and manage multi-turn interactions:
+
+```bash
+jarvis ask --agent orchestrator "What is 2+2?"
+jarvis ask --agent orchestrator --tools calculator,think "Calculate sqrt(144) + 3^2"
+jarvis ask --agent simple "Hello"
+```
+
+### Usage Examples
+
+```bash
+# Basic query
+jarvis ask "What is machine learning?"
+
+# Specify a model
+jarvis ask -m qwen3:8b "Summarize this concept"
+
+# Use the orchestrator agent with tools
+jarvis ask --agent orchestrator --tools calculator "What is 15% of 340?"
+
+# Get JSON output
+jarvis ask --json "Hello"
+
+# Disable memory context injection
+jarvis ask --no-context "Tell me about Python"
+
+# Specify router policy and temperature
+jarvis ask --router heuristic -t 0.3 "Write a haiku"
+
+# Set maximum token generation
+jarvis ask --max-tokens 2048 "Write a detailed essay about AI"
+```
+
+### JSON Output Format
+
+When using `--json` in **direct mode**, the output includes:
+
+```json
+{
+ "content": "The response text...",
+ "usage": {
+ "prompt_tokens": 12,
+ "completion_tokens": 85,
+ "total_tokens": 97
+ }
+}
+```
+
+When using `--json` in **agent mode**, the output includes:
+
+```json
+{
+ "content": "The response text...",
+ "turns": 3,
+ "tool_results": [
+ {
+ "tool_name": "calculator",
+ "content": "51.0",
+ "success": true
+ }
+ ]
+}
+```
+
+---
+
+## `jarvis model`
+
+Manage and inspect language models available on running engines.
+
+### `jarvis model list`
+
+List all models available from running inference engines, displayed as a Rich table with model parameters, context length, and VRAM requirements.
+
+```bash
+jarvis model list
+```
+
+**Example output:**
+
+```
+ Available Models
+┌─────────┬────────────────┬────────┬─────────┬──────┐
+│ Engine │ Model │ Params │ Context │ VRAM │
+├─────────┼────────────────┼────────┼─────────┼──────┤
+│ ollama │ qwen3:8b │ 8B │ 32,768 │ 6GB │
+│ ollama │ llama3.2:3b │ 3B │ 8,192 │ 3GB │
+└─────────┴────────────────┴────────┴─────────┴──────┘
+```
+
+### `jarvis model info `
+
+Show detailed information about a specific model.
+
+```bash
+jarvis model info qwen3:8b
+```
+
+**Example output:**
+
+```
+┌─ Qwen 3 8B ──────────────────────────────┐
+│ Model ID: qwen3:8b │
+│ Name: Qwen 3 8B │
+│ Parameters: 8B │
+│ Context: 32,768 │
+│ Quantization: none │
+│ Min VRAM: 6GB │
+│ Engines: ollama, vllm │
+│ Provider: Alibaba │
+│ API Key: not required │
+└───────────────────────────────────────────┘
+```
+
+### `jarvis model pull `
+
+Download a model via Ollama. Shows a progress bar during download.
+
+```bash
+jarvis model pull qwen3:8b
+```
+
+!!! note
+ The `pull` command requires a running Ollama instance. It connects to the Ollama API at the host configured in your `config.toml`.
+
+---
+
+## `jarvis memory`
+
+Manage the document memory store for retrieval-augmented generation.
+
+### `jarvis memory index `
+
+Index documents from a file or directory into the memory store.
+
+```bash
+jarvis memory index ./docs/
+jarvis memory index ./notes.md
+jarvis memory index ./data/ --chunk-size 256 --chunk-overlap 32
+jarvis memory index ./docs/ --backend sqlite
+```
+
+| Option | Type | Default | Description |
+|-----------------------------|--------|---------|--------------------------------------|
+| `--backend`, `-b` | string | config | Override the default memory backend |
+| `--chunk-size` | int | `512` | Chunk size in tokens |
+| `--chunk-overlap` | int | `64` | Overlap between chunks in tokens |
+
+The ingestion pipeline supports text, markdown, code files, and PDF (with `pdfplumber` installed). Binary files and hidden directories are automatically skipped.
+
+### `jarvis memory search `
+
+Search the memory store for relevant document chunks.
+
+```bash
+jarvis memory search "machine learning basics"
+jarvis memory search -k 10 "neural networks"
+jarvis memory search --backend faiss "embeddings"
+```
+
+| Option | Type | Default | Description |
+|--------------------|--------|---------|--------------------------------------|
+| `--top-k`, `-k` | int | `5` | Number of results to return |
+| `--backend`, `-b` | string | config | Override the default memory backend |
+
+Results are displayed in a table with rank, score, source file, and a content preview.
+
+### `jarvis memory stats`
+
+Show memory store statistics including document count and database size.
+
+```bash
+jarvis memory stats
+jarvis memory stats --backend sqlite
+```
+
+| Option | Type | Default | Description |
+|--------------------|--------|---------|--------------------------------------|
+| `--backend`, `-b` | string | config | Override the default memory backend |
+
+---
+
+## `jarvis telemetry`
+
+Query and manage inference telemetry data stored in SQLite.
+
+### `jarvis telemetry stats`
+
+Show aggregated telemetry statistics including total calls, tokens, cost, and latency, broken down by model and engine.
+
+```bash
+jarvis telemetry stats
+jarvis telemetry stats -n 5 # Show top 5 models
+```
+
+| Option | Type | Default | Description |
+|-----------------|------|---------|-------------------------------|
+| `-n`, `--top` | int | `10` | Number of top models to show |
+
+### `jarvis telemetry export`
+
+Export raw telemetry records in JSON or CSV format.
+
+```bash
+jarvis telemetry export # JSON to stdout
+jarvis telemetry export --format csv # CSV to stdout
+jarvis telemetry export --format json -o data.json # JSON to file
+jarvis telemetry export -f csv -o metrics.csv # CSV to file
+```
+
+| Option | Type | Default | Description |
+|-----------------------|--------|----------|---------------------------------|
+| `-f`, `--format` | choice | `json` | Output format: `json` or `csv` |
+| `-o`, `--output` | path | stdout | Output file path |
+
+### `jarvis telemetry clear`
+
+Delete all telemetry records from the database.
+
+```bash
+jarvis telemetry clear # Interactive confirmation
+jarvis telemetry clear --yes # Skip confirmation
+```
+
+| Option | Type | Default | Description |
+|----------------|------|---------|-------------------------------|
+| `-y`, `--yes` | flag | off | Skip confirmation prompt |
+
+!!! warning
+ This permanently deletes all stored telemetry data. Use `--yes` to skip the confirmation prompt in automated scripts.
+
+---
+
+## `jarvis bench`
+
+Run inference benchmarks against a running engine.
+
+### `jarvis bench run`
+
+Execute benchmarks and report results.
+
+```bash
+jarvis bench run # Run all benchmarks, 10 samples
+jarvis bench run -n 20 # 20 samples per benchmark
+jarvis bench run -b latency # Only the latency benchmark
+jarvis bench run -b throughput -n 50 --json # Throughput, 50 samples, JSON output
+jarvis bench run -o results.jsonl # Write JSONL results to file
+jarvis bench run -m qwen3:8b -e ollama # Specific model and engine
+```
+
+| Option | Type | Default | Description |
+|----------------------------|--------|---------|------------------------------------------|
+| `-m`, `--model MODEL` | string | auto | Model to benchmark |
+| `-e`, `--engine ENGINE` | string | auto | Engine backend |
+| `-n`, `--samples N` | int | `10` | Number of samples per benchmark |
+| `-b`, `--benchmark NAME` | string | all | Specific benchmark to run |
+| `-o`, `--output PATH` | path | none | Write JSONL results to file |
+| `--json` | flag | off | Output JSON summary to stdout |
+
+Available benchmarks:
+
+- **latency** -- Measures per-call inference latency (mean, p50, p95, min, max)
+- **throughput** -- Measures tokens-per-second throughput
+
+---
+
+## `jarvis serve`
+
+Start an OpenAI-compatible API server.
+
+```bash
+jarvis serve # Default host/port from config
+jarvis serve --port 8000 # Custom port
+jarvis serve --host 0.0.0.0 --port 9000 # Bind to all interfaces
+jarvis serve --model qwen3:8b # Specify default model
+jarvis serve --agent orchestrator # Route requests through an agent
+```
+
+| Option | Type | Default | Description |
+|--------------------------|--------|---------|------------------------------------------|
+| `--host HOST` | string | config | Bind address |
+| `--port PORT` | int | config | Port number |
+| `-e`, `--engine ENGINE` | string | auto | Engine backend |
+| `-m`, `--model MODEL` | string | config | Default model for inference |
+| `-a`, `--agent AGENT` | string | none | Agent for non-streaming requests |
+
+!!! note "Server Dependencies"
+ The `serve` command requires the server extra:
+
+ ```bash
+ pip install 'openjarvis[server]'
+ ```
+
+ This installs FastAPI, uvicorn, and related dependencies.
+
+### API Endpoints
+
+The server exposes the following OpenAI-compatible endpoints:
+
+| Method | Path | Description |
+|--------|--------------------------|--------------------------------|
+| POST | `/v1/chat/completions` | Chat completions (streaming & non-streaming) |
+| GET | `/v1/models` | List available models |
+| GET | `/health` | Health check |
+
+**Example with curl:**
+
+```bash
+curl http://localhost:8000/v1/chat/completions \
+ -H "Content-Type: application/json" \
+ -d '{
+ "model": "qwen3:8b",
+ "messages": [{"role": "user", "content": "Hello!"}]
+ }'
+```
+
+When an agent is configured (e.g., `--agent orchestrator`), non-streaming requests are routed through the agent with access to all registered tools. For tool-capable agents (`orchestrator`, `react`, `openhands`), all registered tools are automatically loaded and made available.
diff --git a/docs/user-guide/memory.md b/docs/user-guide/memory.md
new file mode 100644
index 00000000..27324570
--- /dev/null
+++ b/docs/user-guide/memory.md
@@ -0,0 +1,365 @@
+# Memory
+
+The memory system provides persistent, searchable document storage for retrieval-augmented generation (RAG). It supports multiple retrieval backends, a configurable chunking pipeline, document ingestion from files and directories, and automatic context injection into prompts.
+
+## Architecture
+
+```
+Documents --> Chunking Pipeline --> Memory Backend --> Context Injection --> Prompt
+ (files) (split + overlap) (store + index) (retrieve + format) (to LLM)
+```
+
+---
+
+## MemoryBackend ABC
+
+All memory backends implement the `MemoryBackend` abstract base class.
+
+```python
+class MemoryBackend(ABC):
+ backend_id: str
+
+ def store(self, content: str, *, source: str = "", metadata: dict | None = None) -> str:
+ """Persist content and return a unique document ID."""
+
+ def retrieve(self, query: str, *, top_k: int = 5, **kwargs) -> list[RetrievalResult]:
+ """Search for query and return the top-k results."""
+
+ def delete(self, doc_id: str) -> bool:
+ """Delete a document by ID. Return True if it existed."""
+
+ def clear(self) -> None:
+ """Remove all stored documents."""
+```
+
+### RetrievalResult
+
+Each retrieval returns a list of `RetrievalResult` objects:
+
+| Field | Type | Description |
+|------------|------------------|--------------------------------------------|
+| `content` | `str` | The retrieved text chunk |
+| `score` | `float` | Relevance score (higher is better) |
+| `source` | `str` | Originating file path or identifier |
+| `metadata` | `dict[str, Any]` | Additional metadata (chunk index, etc.) |
+
+---
+
+## Backends
+
+### SQLite / FTS5 (Default)
+
+**Registry key:** `sqlite`
+
+The default backend using SQLite's built-in FTS5 full-text search extension. Zero external dependencies -- uses Python's standard `sqlite3` module.
+
+- **Scoring:** BM25 ranking via FTS5 MATCH queries
+- **Persistence:** SQLite database file (default: `~/.openjarvis/memory.db`)
+- **Dependencies:** None (built into Python)
+
+```python
+from openjarvis.core.registry import MemoryRegistry
+
+backend = MemoryRegistry.create("sqlite", db_path="./memory.db")
+doc_id = backend.store("Hello world", source="test.txt")
+results = backend.retrieve("hello")
+backend.close()
+```
+
+!!! tip "When to use SQLite/FTS5"
+ Use this backend when you want zero-configuration setup, keyword-based search is sufficient, and you need persistent storage across restarts. It works well for small to medium document collections.
+
+### FAISS
+
+**Registry key:** `faiss`
+
+Dense neural retrieval using Facebook AI Similarity Search. Embeds documents and queries into dense vectors and retrieves by cosine similarity.
+
+- **Scoring:** Cosine similarity via inner-product search on L2-normalized vectors
+- **Persistence:** In-memory only (data is lost on restart)
+- **Dependencies:** `faiss-cpu` (or `faiss-gpu`), `sentence-transformers`
+
+```bash
+pip install faiss-cpu sentence-transformers
+```
+
+```python
+backend = MemoryRegistry.create("faiss")
+doc_id = backend.store("Neural networks are computational models")
+results = backend.retrieve("deep learning architectures")
+```
+
+!!! tip "When to use FAISS"
+ Use this backend when you need semantic search (finding conceptually similar content even without exact keyword matches). Best for use cases where you can re-index on each run since data is not persisted.
+
+### ColBERTv2
+
+**Registry key:** `colbert`
+
+Late-interaction retrieval using ColBERT's token-level embeddings with MaxSim scoring. Provides the highest retrieval quality among the available backends.
+
+- **Scoring:** MaxSim -- for each query token, take the maximum cosine similarity across all document tokens, then sum
+- **Persistence:** In-memory only
+- **Dependencies:** `colbert-ai`, `torch`
+
+```bash
+pip install colbert-ai torch
+```
+
+```python
+backend = MemoryRegistry.create(
+ "colbert",
+ checkpoint="colbert-ir/colbertv2.0",
+ device="cpu",
+)
+```
+
+| Parameter | Default | Description |
+|--------------|----------------------------|-------------------------------------|
+| `checkpoint` | `"colbert-ir/colbertv2.0"` | ColBERT model checkpoint |
+| `device` | `"cpu"` | Computation device (`cpu` or `cuda`) |
+
+!!! tip "When to use ColBERTv2"
+ Use this backend when retrieval quality is the top priority and you have the compute resources for it. The checkpoint is lazily loaded on first use to avoid slow imports. Best for research and evaluation workloads.
+
+### BM25
+
+**Registry key:** `bm25`
+
+Classic probabilistic ranking using the BM25 Okapi algorithm. In-memory implementation using the `rank_bm25` library.
+
+- **Scoring:** BM25 Okapi term-frequency scoring
+- **Persistence:** In-memory only
+- **Dependencies:** `rank-bm25`
+
+```bash
+pip install rank-bm25
+```
+
+```python
+backend = MemoryRegistry.create("bm25")
+backend.store("Python is a programming language", source="intro.txt")
+results = backend.retrieve("programming language")
+```
+
+!!! tip "When to use BM25"
+ Use this backend when you want classic keyword-based retrieval without database dependencies. Useful as the sparse component in a hybrid retrieval setup.
+
+### Hybrid (RRF Fusion)
+
+**Registry key:** `hybrid`
+
+Combines a sparse retriever and a dense retriever using Reciprocal Rank Fusion (RRF). Documents are stored in both sub-backends, and retrieval results are merged.
+
+- **Scoring:** `RRF_score(d) = sum(weight_i / (k + rank_i(d)))` across both ranked lists
+- **Persistence:** Depends on sub-backends
+- **Dependencies:** Depends on sub-backends
+
+```python
+from openjarvis.memory.bm25 import BM25Memory
+from openjarvis.memory.faiss_backend import FAISSMemory
+
+sparse = BM25Memory()
+dense = FAISSMemory()
+
+backend = MemoryRegistry.create(
+ "hybrid",
+ sparse=sparse,
+ dense=dense,
+ k=60,
+ sparse_weight=1.0,
+ dense_weight=1.0,
+)
+```
+
+| Parameter | Default | Description |
+|-----------------|---------|------------------------------------------|
+| `sparse` | -- | Sparse retrieval backend (e.g., BM25) |
+| `dense` | -- | Dense retrieval backend (e.g., FAISS) |
+| `k` | `60` | RRF constant |
+| `sparse_weight` | `1.0` | Weight for sparse retriever results |
+| `dense_weight` | `1.0` | Weight for dense retriever results |
+
+The hybrid backend over-fetches (3x `top_k`) from each sub-backend before applying fusion to improve result quality.
+
+!!! tip "When to use Hybrid"
+ Use this backend when you want the best of both keyword matching and semantic similarity. The RRF fusion approach is robust and does not require tuning score distributions across different retrieval methods.
+
+---
+
+## Backend Comparison
+
+| Backend | Search Type | Persistence | Dependencies | Quality | Speed |
+|-------------|-------------------|-------------|----------------------|----------|----------|
+| SQLite/FTS5 | Keyword (BM25) | Yes | None | Good | Fast |
+| FAISS | Dense (cosine) | No | faiss, transformers | Better | Fast |
+| ColBERTv2 | Late interaction | No | colbert-ai, torch | Best | Slower |
+| BM25 | Keyword (Okapi) | No | rank-bm25 | Good | Fast |
+| Hybrid | Fusion (RRF) | Mixed | Sub-backend deps | Better | Medium |
+
+---
+
+## Chunking Pipeline
+
+Documents are split into chunks before storage using a configurable pipeline. The chunker respects paragraph boundaries when possible.
+
+### ChunkConfig
+
+| Field | Type | Default | Description |
+|-----------------|-------|---------|------------------------------------------|
+| `chunk_size` | `int` | `512` | Target chunk size in whitespace tokens |
+| `chunk_overlap` | `int` | `64` | Overlap between consecutive chunks |
+| `min_chunk_size`| `int` | `50` | Minimum chunk size (smaller chunks are discarded) |
+
+### How Chunking Works
+
+1. The document is split into paragraphs (separated by double newlines).
+2. Paragraphs are accumulated until the token count exceeds `chunk_size`.
+3. The accumulated content is emitted as a chunk.
+4. The last `chunk_overlap` tokens are retained as context for the next chunk.
+5. Paragraphs exceeding `chunk_size` are split into fixed-size windows with overlap.
+
+### Chunk Output
+
+Each chunk is a `Chunk` object with:
+
+| Field | Type | Description |
+|------------|------------------|------------------------------------------|
+| `content` | `str` | The chunk text |
+| `source` | `str` | Originating file path |
+| `offset` | `int` | Token offset within the document |
+| `index` | `int` | Sequential chunk index |
+| `metadata` | `dict[str, Any]` | Additional metadata |
+
+---
+
+## Document Ingestion
+
+The `ingest_path()` function reads files or recursively walks directories, producing chunks ready for storage.
+
+### Supported File Types
+
+| Type | Extensions |
+|----------|-------------------------------------------------------------|
+| Text | `.txt` and other plain text files |
+| Markdown | `.md`, `.markdown`, `.mdx` |
+| Code | `.py`, `.js`, `.ts`, `.rs`, `.go`, `.java`, `.c`, `.cpp`, `.rb`, `.sh`, `.yaml`, `.json`, `.html`, `.css`, and more |
+| PDF | `.pdf` (requires `pdfplumber`: `pip install openjarvis[memory-pdf]`) |
+
+### Automatic Skipping
+
+The ingestion pipeline automatically skips:
+
+- Hidden files and directories (starting with `.`)
+- Common non-content directories: `__pycache__`, `node_modules`, `.venv`, `.git`, etc.
+- Binary files: images, audio, video, archives, compiled files
+- Files that cannot be read (permission errors, encoding issues)
+
+### Usage
+
+```python
+from pathlib import Path
+from openjarvis.memory.chunking import ChunkConfig
+from openjarvis.memory.ingest import ingest_path
+
+# Default chunking
+chunks = ingest_path(Path("./docs/"))
+
+# Custom chunking
+config = ChunkConfig(chunk_size=256, chunk_overlap=32)
+chunks = ingest_path(Path("./notes.md"), config=config)
+
+print(f"Produced {len(chunks)} chunks")
+for chunk in chunks[:3]:
+ print(f" [{chunk.index}] {chunk.source}: {chunk.content[:60]}...")
+```
+
+---
+
+## Context Injection
+
+When memory context injection is enabled (the default), queries are automatically augmented with relevant retrieved documents before being sent to the model. Each retrieved passage includes source attribution.
+
+### ContextConfig
+
+| Field | Type | Default | Description |
+|---------------------|---------|---------|--------------------------------------------------|
+| `enabled` | `bool` | `True` | Whether context injection is active |
+| `top_k` | `int` | `5` | Number of results to retrieve |
+| `min_score` | `float` | `0.1` | Minimum relevance score threshold |
+| `max_context_tokens`| `int` | `2048` | Maximum total tokens in injected context |
+
+### How It Works
+
+1. The user's query is searched against the memory backend.
+2. Results below `min_score` are filtered out.
+3. Results are truncated to fit within `max_context_tokens`.
+4. A system message is prepended to the conversation with the formatted context:
+
+```
+The following context was retrieved from the knowledge base.
+Use it to inform your response, citing sources where applicable:
+
+[Source: docs/intro.md] OpenJarvis is a modular AI framework...
+
+[Source: docs/config.md] Configuration is stored in TOML format...
+```
+
+### Disabling Context Injection
+
+=== "CLI"
+
+ ```bash
+ jarvis ask --no-context "Tell me about Python"
+ ```
+
+=== "Python SDK"
+
+ ```python
+ response = j.ask("Tell me about Python", context=False)
+ ```
+
+---
+
+## CLI Usage
+
+```bash
+# Index a directory
+jarvis memory index ./docs/
+
+# Index with custom chunking
+jarvis memory index ./notes/ --chunk-size 256 --chunk-overlap 32
+
+# Search the memory store
+jarvis memory search "machine learning"
+
+# Search with more results
+jarvis memory search -k 10 "neural networks"
+
+# Show memory statistics
+jarvis memory stats
+```
+
+## SDK Usage
+
+```python
+from openjarvis import Jarvis
+
+j = Jarvis()
+
+# Index documents
+result = j.memory.index("./docs/", chunk_size=512, chunk_overlap=64)
+print(f"Indexed {result['chunks']} chunks")
+
+# Search
+results = j.memory.search("configuration", top_k=3)
+for r in results:
+ print(f" [{r['score']:.4f}] {r['source']}: {r['content'][:80]}...")
+
+# Statistics
+stats = j.memory.stats()
+print(f"Backend: {stats['backend']}, Documents: {stats.get('count', 'N/A')}")
+
+# Clean up
+j.close()
+```
diff --git a/docs/user-guide/python-sdk.md b/docs/user-guide/python-sdk.md
new file mode 100644
index 00000000..56982292
--- /dev/null
+++ b/docs/user-guide/python-sdk.md
@@ -0,0 +1,376 @@
+# Python SDK
+
+The OpenJarvis Python SDK provides a high-level interface for interacting with local inference engines, managing memory, and running agent workflows. The primary entry point is the `Jarvis` class.
+
+## Installation
+
+```bash
+pip install openjarvis
+```
+
+## Quick Start
+
+```python
+from openjarvis import Jarvis
+
+j = Jarvis()
+response = j.ask("What is the capital of France?")
+print(response)
+j.close()
+```
+
+---
+
+## Jarvis Class
+
+### Constructor
+
+```python
+Jarvis(
+ *,
+ config: JarvisConfig | None = None,
+ config_path: str | None = None,
+ engine_key: str | None = None,
+ model: str | None = None,
+)
+```
+
+| Parameter | Type | Default | Description |
+|---------------|------------------|---------|----------------------------------------------------------------|
+| `config` | `JarvisConfig` | `None` | Provide a pre-built configuration object |
+| `config_path` | `str` | `None` | Path to a TOML configuration file |
+| `engine_key` | `str` | `None` | Override the engine backend (`"ollama"`, `"vllm"`, etc.) |
+| `model` | `str` | `None` | Override the default model (e.g., `"qwen3:8b"`) |
+
+If no `config` or `config_path` is provided, the SDK loads configuration from the default location (`~/.openjarvis/config.toml`), falling back to built-in defaults.
+
+**Examples:**
+
+```python
+# Default configuration — auto-detects engine
+j = Jarvis()
+
+# Override the model
+j = Jarvis(model="qwen3:8b")
+
+# Override the engine
+j = Jarvis(engine_key="ollama")
+
+# Load from a specific config file
+j = Jarvis(config_path="/path/to/config.toml")
+```
+
+### Properties
+
+| Property | Type | Description |
+|-----------|----------------|-----------------------------------|
+| `config` | `JarvisConfig` | The active configuration object |
+| `version` | `str` | The OpenJarvis version string |
+| `memory` | `MemoryHandle` | Proxy for memory operations |
+
+---
+
+## `ask()` Method
+
+Send a query and receive a plain-text response.
+
+```python
+ask(
+ query: str,
+ *,
+ model: str | None = None,
+ agent: str | None = None,
+ tools: list[str] | None = None,
+ temperature: float = 0.7,
+ max_tokens: int = 1024,
+ context: bool = True,
+) -> str
+```
+
+| Parameter | Type | Default | Description |
+|---------------|--------------|---------|------------------------------------------------------|
+| `query` | `str` | -- | The question or prompt to send |
+| `model` | `str` | `None` | Override the model for this call |
+| `agent` | `str` | `None` | Route through an agent (`"simple"`, `"orchestrator"`) |
+| `tools` | `list[str]` | `None` | Tool names to enable (requires agent mode) |
+| `temperature` | `float` | `0.7` | Sampling temperature |
+| `max_tokens` | `int` | `1024` | Maximum tokens to generate |
+| `context` | `bool` | `True` | Whether to inject memory context |
+
+**Returns:** A `str` containing the model's response text.
+
+**Examples:**
+
+```python
+# Simple query
+response = j.ask("What is machine learning?")
+
+# Override model for this call
+response = j.ask("Hello", model="llama3.2:3b")
+
+# Disable memory context injection
+response = j.ask("Tell me about Python", context=False)
+
+# Adjust generation parameters
+response = j.ask("Write a haiku", temperature=0.3, max_tokens=50)
+```
+
+---
+
+## `ask_full()` Method
+
+Send a query and receive a detailed result dictionary with metadata.
+
+```python
+ask_full(
+ query: str,
+ *,
+ model: str | None = None,
+ agent: str | None = None,
+ tools: list[str] | None = None,
+ temperature: float = 0.7,
+ max_tokens: int = 1024,
+ context: bool = True,
+) -> dict[str, Any]
+```
+
+The parameters are identical to `ask()`.
+
+**Returns:** A dictionary with the following keys:
+
+=== "Direct Mode"
+
+ | Key | Type | Description |
+ |-----------|--------|------------------------------------------|
+ | `content` | `str` | The response text |
+ | `usage` | `dict` | Token usage (`prompt_tokens`, `completion_tokens`, `total_tokens`) |
+ | `model` | `str` | The model used |
+ | `engine` | `str` | The engine backend used |
+
+=== "Agent Mode"
+
+ | Key | Type | Description |
+ |----------------|--------------|------------------------------------------|
+ | `content` | `str` | The response text |
+ | `usage` | `dict` | Token usage (may be empty in agent mode) |
+ | `tool_results` | `list[dict]` | Tool execution results |
+ | `turns` | `int` | Number of agent turns taken |
+ | `model` | `str` | The model used |
+ | `engine` | `str` | The engine backend used |
+
+**Example:**
+
+```python
+result = j.ask_full("What is 2+2?")
+print(result["content"]) # "4"
+print(result["model"]) # "qwen3:8b"
+print(result["engine"]) # "ollama"
+print(result["usage"]) # {"prompt_tokens": 10, ...}
+```
+
+---
+
+## Agent Mode
+
+Pass the `agent` parameter to route queries through an agent. Agents can manage multi-turn conversations and use tools.
+
+```python
+# Simple agent — single turn, no tools
+response = j.ask("Hello", agent="simple")
+
+# Orchestrator agent — multi-turn with tool calling
+response = j.ask(
+ "What is sqrt(144) + 3^2?",
+ agent="orchestrator",
+ tools=["calculator", "think"],
+)
+```
+
+When using agent mode with `ask_full()`, the result includes `tool_results` showing each tool invocation:
+
+```python
+result = j.ask_full(
+ "Calculate 15% of 340",
+ agent="orchestrator",
+ tools=["calculator"],
+)
+
+print(result["content"]) # "15% of 340 is 51.0"
+print(result["turns"]) # 2
+print(result["tool_results"])
+# [{"tool_name": "calculator", "content": "51.0", "success": True}]
+```
+
+Available agents: `simple`, `orchestrator`, `openclaw`, `custom`
+
+Available tools: `calculator`, `think`, `retrieval`, `llm`, `file_read`
+
+---
+
+## MemoryHandle
+
+The `Jarvis.memory` attribute provides a `MemoryHandle` for document indexing, search, and statistics. The memory backend is lazily initialized on first use.
+
+### `index()`
+
+Index a file or directory into the memory store.
+
+```python
+index(
+ path: str,
+ *,
+ chunk_size: int = 512,
+ chunk_overlap: int = 64,
+) -> dict[str, Any]
+```
+
+| Parameter | Type | Default | Description |
+|-----------------|-------|---------|---------------------------------------|
+| `path` | `str` | -- | Path to a file or directory to index |
+| `chunk_size` | `int` | `512` | Chunk size in tokens |
+| `chunk_overlap` | `int` | `64` | Overlap between chunks in tokens |
+
+**Returns:** A dictionary with `chunks` (count), `doc_ids` (list), and `path`.
+
+```python
+result = j.memory.index("./docs/")
+print(f"Indexed {result['chunks']} chunks")
+# Indexed 42 chunks
+
+# Custom chunking parameters
+result = j.memory.index("./notes/", chunk_size=256, chunk_overlap=32)
+```
+
+### `search()`
+
+Search the memory store for relevant chunks.
+
+```python
+search(
+ query: str,
+ *,
+ top_k: int = 5,
+) -> list[dict[str, Any]]
+```
+
+| Parameter | Type | Default | Description |
+|-----------|-------|---------|--------------------------------|
+| `query` | `str` | -- | The search query |
+| `top_k` | `int` | `5` | Number of results to return |
+
+**Returns:** A list of dictionaries, each containing `content`, `score`, `source`, and `metadata`.
+
+```python
+results = j.memory.search("neural networks")
+for r in results:
+ print(f"[{r['score']:.4f}] {r['source']}: {r['content'][:80]}...")
+```
+
+### `stats()`
+
+Return memory backend statistics.
+
+```python
+stats() -> dict[str, Any]
+```
+
+**Returns:** A dictionary with `backend` (name) and `count` (document count, if available).
+
+```python
+info = j.memory.stats()
+print(f"Backend: {info['backend']}, Documents: {info.get('count', 'N/A')}")
+```
+
+### `close()`
+
+Release the memory backend and its resources.
+
+```python
+j.memory.close()
+```
+
+---
+
+## Model and Engine Discovery
+
+### `list_models()`
+
+Return a list of model identifiers available on the active engine.
+
+```python
+models = j.list_models()
+print(models) # ["qwen3:8b", "llama3.2:3b", ...]
+```
+
+### `list_engines()`
+
+Return a list of registered engine keys.
+
+```python
+engines = j.list_engines()
+print(engines) # ["ollama", "vllm", "llamacpp", ...]
+```
+
+---
+
+## Resource Management
+
+### `close()`
+
+Release all resources held by the `Jarvis` instance, including the memory backend, telemetry store, and engine connection.
+
+```python
+j.close()
+```
+
+!!! tip "Context Manager Pattern"
+ While `Jarvis` does not implement `__enter__`/`__exit__` directly, you should always call `close()` when done to free database connections and other resources:
+
+ ```python
+ j = Jarvis()
+ try:
+ response = j.ask("Hello")
+ print(response)
+ finally:
+ j.close()
+ ```
+
+---
+
+## Complete Example
+
+```python
+from openjarvis import Jarvis
+
+# Initialize with auto-detected engine
+j = Jarvis(model="qwen3:8b")
+
+# Index documents for context-augmented responses
+result = j.memory.index("./docs/")
+print(f"Indexed {result['chunks']} chunks from {result['path']}")
+
+# Simple query with memory context
+response = j.ask("What are the main features?")
+print(response)
+
+# Detailed query with agent and tools
+full_result = j.ask_full(
+ "Calculate the square root of 256 and add 10",
+ agent="orchestrator",
+ tools=["calculator"],
+)
+print(f"Answer: {full_result['content']}")
+print(f"Turns: {full_result['turns']}")
+print(f"Tools used: {[t['tool_name'] for t in full_result['tool_results']]}")
+
+# Search memory directly
+results = j.memory.search("configuration")
+for r in results:
+ print(f" [{r['score']:.3f}] {r['source']}")
+
+# List available models
+print("Models:", j.list_models())
+
+# Clean up
+j.close()
+```
diff --git a/docs/user-guide/telemetry.md b/docs/user-guide/telemetry.md
new file mode 100644
index 00000000..e770545d
--- /dev/null
+++ b/docs/user-guide/telemetry.md
@@ -0,0 +1,423 @@
+# Telemetry & Traces
+
+OpenJarvis has two complementary observability systems: **telemetry** for per-inference metrics and **traces** for full interaction-level recording. Together, they provide comprehensive insight into system behavior and power the learning system's routing policy updates.
+
+---
+
+## Telemetry
+
+The telemetry system records metrics for every inference call -- latency, token counts, cost, and energy consumption. Data is stored in SQLite and can be queried, exported, and aggregated.
+
+### TelemetryRecord
+
+Each inference call produces a `TelemetryRecord` with the following fields:
+
+| Field | Type | Description |
+|----------------------|------------------|------------------------------------------|
+| `timestamp` | `float` | Unix timestamp of the call |
+| `model_id` | `str` | Model identifier |
+| `engine` | `str` | Engine backend used |
+| `agent` | `str` | Agent used (if any) |
+| `prompt_tokens` | `int` | Input tokens consumed |
+| `completion_tokens` | `int` | Output tokens generated |
+| `total_tokens` | `int` | Total tokens (prompt + completion) |
+| `latency_seconds` | `float` | Wall-clock inference time |
+| `ttft` | `float` | Time to first token |
+| `cost_usd` | `float` | Estimated cost in USD |
+| `energy_joules` | `float` | Estimated energy consumption |
+| `power_watts` | `float` | Power draw during inference |
+| `metadata` | `dict[str, Any]` | Additional metadata |
+
+### TelemetryStore
+
+The `TelemetryStore` is an append-only SQLite database that persists telemetry records. It integrates with the event bus to capture records automatically.
+
+```python
+from openjarvis.telemetry.store import TelemetryStore
+from openjarvis.core.events import EventBus
+
+bus = EventBus()
+store = TelemetryStore(db_path="~/.openjarvis/telemetry.db")
+store.subscribe_to_bus(bus)
+
+# Records are now captured automatically when TELEMETRY_RECORD events fire.
+# No manual recording needed -- instrumented_generate() handles this.
+
+store.close()
+```
+
+The store subscribes to `TELEMETRY_RECORD` events on the event bus. When the `instrumented_generate()` wrapper is used (which happens automatically in both CLI and SDK), telemetry records are published and stored without any manual intervention.
+
+### `instrumented_generate()`
+
+This wrapper function calls `engine.generate()` and automatically publishes telemetry events:
+
+1. Publishes `INFERENCE_START` with model and engine info.
+2. Calls the engine and measures wall-clock latency.
+3. Extracts token usage from the engine response.
+4. Creates a `TelemetryRecord` from the measurements.
+5. Publishes `INFERENCE_END` and `TELEMETRY_RECORD` events.
+
+All CLI commands and SDK methods use this wrapper, so telemetry is recorded transparently.
+
+### TelemetryAggregator
+
+The `TelemetryAggregator` provides read-only query and aggregation methods over stored telemetry data.
+
+```python
+from openjarvis.telemetry.aggregator import TelemetryAggregator
+
+agg = TelemetryAggregator(db_path="~/.openjarvis/telemetry.db")
+
+# Overall summary
+summary = agg.summary()
+print(f"Total calls: {summary.total_calls}")
+print(f"Total tokens: {summary.total_tokens}")
+print(f"Total cost: ${summary.total_cost:.6f}")
+
+# Per-model breakdown
+for ms in agg.per_model_stats():
+ print(f" {ms.model_id}: {ms.call_count} calls, {ms.avg_latency:.3f}s avg")
+
+# Per-engine breakdown
+for es in agg.per_engine_stats():
+ print(f" {es.engine}: {es.call_count} calls, {es.total_tokens} tokens")
+
+# Top models by usage
+top = agg.top_models(n=5)
+
+# Export raw records
+records = agg.export_records()
+
+# Time-range filtering (Unix timestamps)
+recent = agg.summary(since=1700000000.0)
+
+# Clear all records
+count = agg.clear()
+print(f"Deleted {count} records")
+
+agg.close()
+```
+
+#### Aggregation Methods
+
+| Method | Returns | Description |
+|---------------------|--------------------|--------------------------------------------|
+| `summary()` | `AggregatedStats` | Total calls, tokens, cost, latency + per-model and per-engine breakdowns |
+| `per_model_stats()` | `list[ModelStats]` | Call count, tokens, latency, cost grouped by model |
+| `per_engine_stats()`| `list[EngineStats]` | Call count, tokens, latency, cost grouped by engine |
+| `top_models(n)` | `list[ModelStats]` | Top N models by call count |
+| `export_records()` | `list[dict]` | All records as plain dictionaries |
+| `record_count()` | `int` | Total number of stored records |
+| `clear()` | `int` | Delete all records, return count |
+
+All query methods accept optional `since` and `until` parameters (Unix timestamps) for time-range filtering.
+
+#### Data Classes
+
+**ModelStats:**
+
+| Field | Type | Description |
+|--------------------|---------|--------------------------------|
+| `model_id` | `str` | Model identifier |
+| `call_count` | `int` | Total inference calls |
+| `total_tokens` | `int` | Total tokens processed |
+| `prompt_tokens` | `int` | Total input tokens |
+| `completion_tokens`| `int` | Total output tokens |
+| `total_latency` | `float` | Sum of all latencies |
+| `avg_latency` | `float` | Average latency per call |
+| `total_cost` | `float` | Total cost in USD |
+
+**EngineStats:**
+
+| Field | Type | Description |
+|-----------------|---------|--------------------------------|
+| `engine` | `str` | Engine identifier |
+| `call_count` | `int` | Total inference calls |
+| `total_tokens` | `int` | Total tokens processed |
+| `total_latency` | `float` | Sum of all latencies |
+| `avg_latency` | `float` | Average latency per call |
+| `total_cost` | `float` | Total cost in USD |
+
+**AggregatedStats:**
+
+| Field | Type | Description |
+|-----------------|--------------------|--------------------------------|
+| `total_calls` | `int` | Total inference calls |
+| `total_tokens` | `int` | Total tokens across all models |
+| `total_cost` | `float` | Total cost in USD |
+| `total_latency` | `float` | Total latency in seconds |
+| `per_model` | `list[ModelStats]` | Breakdown by model |
+| `per_engine` | `list[EngineStats]` | Breakdown by engine |
+
+### CLI Commands
+
+```bash
+# Show aggregated statistics
+jarvis telemetry stats
+jarvis telemetry stats -n 5 # Top 5 models only
+
+# Export records
+jarvis telemetry export # JSON to stdout
+jarvis telemetry export -f csv # CSV to stdout
+jarvis telemetry export -o data.json # JSON to file
+jarvis telemetry export -f csv -o metrics.csv
+
+# Clear all records
+jarvis telemetry clear # With confirmation prompt
+jarvis telemetry clear --yes # Without confirmation
+```
+
+---
+
+## Traces
+
+While telemetry captures per-inference metrics, the trace system records **complete interaction sequences** -- the full chain of steps an agent takes to handle a query. Traces are the primary input to the learning system.
+
+### What is a Trace?
+
+A `Trace` captures the entire lifecycle of handling a user query:
+
+| Field | Type | Description |
+|--------------------------|--------------------|-----------------------------------------------|
+| `trace_id` | `str` | Unique identifier (auto-generated) |
+| `query` | `str` | The original user query |
+| `agent` | `str` | Agent that handled the query |
+| `model` | `str` | Model used for inference |
+| `engine` | `str` | Engine backend used |
+| `steps` | `list[TraceStep]` | Ordered list of processing steps |
+| `result` | `str` | Final response content |
+| `outcome` | `str` or `None` | `"success"`, `"failure"`, or `None` (unknown) |
+| `feedback` | `float` or `None` | User quality score [0, 1] |
+| `started_at` | `float` | Unix timestamp when processing began |
+| `ended_at` | `float` | Unix timestamp when processing ended |
+| `total_tokens` | `int` | Total tokens across all steps |
+| `total_latency_seconds` | `float` | Total latency across all steps |
+| `metadata` | `dict[str, Any]` | Additional metadata |
+
+### Trace vs Telemetry
+
+| Aspect | Telemetry | Traces |
+|----------------|----------------------------------------|----------------------------------------------|
+| **Scope** | Single inference call | Full interaction (multiple steps) |
+| **Granularity**| Per-call metrics | Step-by-step sequence |
+| **Purpose** | Performance monitoring, cost tracking | Learning, routing optimization, debugging |
+| **Data** | Latency, tokens, cost, energy | Route, retrieve, generate, tool_call, respond |
+| **Storage** | Flat table of records | Traces table + steps table |
+
+### TraceStep
+
+Each step in a trace records a single action the agent took.
+
+| Field | Type | Description |
+|--------------------|------------------|------------------------------------------|
+| `step_type` | `StepType` | Type of step (see below) |
+| `timestamp` | `float` | When the step occurred |
+| `duration_seconds` | `float` | How long the step took |
+| `input` | `dict[str, Any]` | Input data for the step |
+| `output` | `dict[str, Any]` | Output data from the step |
+| `metadata` | `dict[str, Any]` | Additional metadata |
+
+### StepType
+
+| Type | Description | Example Input | Example Output |
+|--------------|--------------------------------------------------|------------------------------|------------------------------------|
+| `route` | Model/agent selection decision | `{"query_type": "math"}` | `{"model": "qwen3:8b"}` |
+| `retrieve` | Memory search for context | `{"query": "topic"}` | `{"num_results": 3}` |
+| `generate` | LLM inference call | `{"model": "qwen3:8b"}` | `{"tokens": 128}` |
+| `tool_call` | Tool execution | `{"tool": "calculator"}` | `{"success": true}` |
+| `respond` | Final response to the user | `{}` | `{"content": "...", "turns": 2}` |
+
+### TraceCollector
+
+The `TraceCollector` wraps any `BaseAgent` to automatically record a `Trace` for every `run()` call. It subscribes to event bus events during execution and converts them into `TraceStep` objects.
+
+```python
+from openjarvis.agents.orchestrator import OrchestratorAgent
+from openjarvis.traces.collector import TraceCollector
+from openjarvis.traces.store import TraceStore
+from openjarvis.core.events import EventBus
+
+bus = EventBus()
+store = TraceStore(db_path="./traces.db")
+
+agent = OrchestratorAgent(engine, model, tools=tools, bus=bus)
+collector = TraceCollector(agent, store=store, bus=bus)
+
+# The trace is recorded automatically
+result = collector.run("What is 2+2?")
+print(result.content)
+# Trace is now saved to the store and published on the bus
+```
+
+**How the collector works:**
+
+1. Subscribes to `INFERENCE_START`, `INFERENCE_END`, `TOOL_CALL_START`, `TOOL_CALL_END`, and `MEMORY_RETRIEVE` events.
+2. Executes the wrapped agent's `run()` method.
+3. Converts captured events into `TraceStep` objects with timing data.
+4. Appends a final `RESPOND` step with the result.
+5. Builds a complete `Trace` object and saves it to the `TraceStore`.
+6. Publishes a `TRACE_COMPLETE` event on the bus.
+7. Unsubscribes from events after the run completes.
+
+### TraceStore
+
+The `TraceStore` is an SQLite-backed database for persisting complete traces with their steps.
+
+```python
+from openjarvis.traces.store import TraceStore
+
+store = TraceStore(db_path="./traces.db")
+
+# Save a trace
+store.save(trace)
+
+# Get a specific trace
+trace = store.get("abc123def456")
+
+# List traces with filters
+traces = store.list_traces(
+ agent="orchestrator",
+ model="qwen3:8b",
+ outcome="success",
+ since=1700000000.0,
+ limit=50,
+)
+
+# Count total traces
+count = store.count()
+
+# Subscribe to event bus for automatic saving
+store.subscribe_to_bus(bus)
+
+store.close()
+```
+
+#### Filtering Options
+
+| Parameter | Type | Description |
+|-----------|---------|------------------------------------------|
+| `agent` | `str` | Filter by agent ID |
+| `model` | `str` | Filter by model ID |
+| `outcome` | `str` | Filter by outcome (`"success"`, `"failure"`) |
+| `since` | `float` | Start of time range (Unix timestamp) |
+| `until` | `float` | End of time range (Unix timestamp) |
+| `limit` | `int` | Maximum number of traces to return (default: 100) |
+
+### TraceAnalyzer
+
+The `TraceAnalyzer` provides read-only aggregated statistics over stored traces. These statistics are used by the learning system to update routing policies.
+
+```python
+from openjarvis.traces.analyzer import TraceAnalyzer
+
+analyzer = TraceAnalyzer(store=trace_store)
+
+# Overall summary
+summary = analyzer.summary()
+print(f"Total traces: {summary.total_traces}")
+print(f"Total steps: {summary.total_steps}")
+print(f"Avg steps/trace: {summary.avg_steps_per_trace:.1f}")
+print(f"Avg latency: {summary.avg_latency:.3f}s")
+print(f"Success rate: {summary.success_rate:.1%}")
+print(f"Step distribution: {summary.step_type_distribution}")
+
+# Per-route statistics (model + agent combinations)
+for rs in analyzer.per_route_stats():
+ print(f" {rs.model}/{rs.agent}: {rs.count} traces, "
+ f"{rs.avg_latency:.3f}s avg, {rs.success_rate:.1%} success")
+
+# Per-tool statistics
+for ts in analyzer.per_tool_stats():
+ print(f" {ts.tool_name}: {ts.call_count} calls, "
+ f"{ts.avg_latency:.3f}s avg, {ts.success_rate:.1%} success")
+
+# Find traces matching query characteristics
+code_traces = analyzer.traces_for_query_type(has_code=True)
+short_traces = analyzer.traces_for_query_type(max_length=100)
+
+# Export traces as plain dicts
+exported = analyzer.export_traces(limit=500)
+```
+
+#### Analysis Methods
+
+| Method | Returns | Description |
+|---------------------------|--------------------|------------------------------------------------------|
+| `summary()` | `TraceSummary` | Overall statistics: counts, averages, distributions |
+| `per_route_stats()` | `list[RouteStats]` | Stats grouped by (model, agent) combinations |
+| `per_tool_stats()` | `list[ToolStats]` | Stats grouped by tool name |
+| `traces_for_query_type()` | `list[Trace]` | Filter traces by query characteristics |
+| `export_traces()` | `list[dict]` | Export traces as serializable dictionaries |
+
+All analysis methods accept optional `since` and `until` parameters for time-range filtering.
+
+#### Data Classes
+
+**TraceSummary:**
+
+| Field | Type | Description |
+|--------------------------|-----------------|------------------------------------------|
+| `total_traces` | `int` | Total number of traces |
+| `total_steps` | `int` | Total steps across all traces |
+| `avg_steps_per_trace` | `float` | Average number of steps per trace |
+| `avg_latency` | `float` | Average total latency per trace |
+| `avg_tokens` | `float` | Average tokens per trace |
+| `success_rate` | `float` | Fraction of evaluated traces that succeeded |
+| `step_type_distribution` | `dict[str, int]`| Count of each step type |
+
+**RouteStats:**
+
+| Field | Type | Description |
+|----------------|----------------|------------------------------------------|
+| `model` | `str` | Model identifier |
+| `agent` | `str` | Agent identifier |
+| `count` | `int` | Number of traces for this route |
+| `avg_latency` | `float` | Average latency for this route |
+| `avg_tokens` | `float` | Average tokens for this route |
+| `success_rate` | `float` | Success rate for this route |
+| `avg_feedback` | `float` or `None` | Average user feedback (if available) |
+
+**ToolStats:**
+
+| Field | Type | Description |
+|----------------|---------|------------------------------------------|
+| `tool_name` | `str` | Tool identifier |
+| `call_count` | `int` | Number of times the tool was called |
+| `avg_latency` | `float` | Average execution latency |
+| `success_rate` | `float` | Fraction of successful executions |
+
+---
+
+## Data Flow
+
+The following diagram shows how telemetry and trace data flows through the system:
+
+```
+User Query
+ |
+ v
+Agent.run() --> EventBus --> TraceCollector (captures steps)
+ | |
+ v v
+Engine.generate() TelemetryStore (captures per-call metrics)
+ |
+ v
+instrumented_generate()
+ |
+ +---> INFERENCE_START event
+ +---> INFERENCE_END event
+ +---> TELEMETRY_RECORD event
+ |
+ v
+TraceCollector
+ |
+ +---> Builds Trace with TraceSteps
+ +---> Saves to TraceStore
+ +---> Publishes TRACE_COMPLETE event
+ |
+ v
+TraceAnalyzer / TelemetryAggregator --> Learning System
+```
+
+Both systems operate transparently -- no manual instrumentation is needed when using the CLI or SDK, as they automatically set up the event bus and telemetry store.
diff --git a/docs/user-guide/tools.md b/docs/user-guide/tools.md
new file mode 100644
index 00000000..1df2a0a3
--- /dev/null
+++ b/docs/user-guide/tools.md
@@ -0,0 +1,391 @@
+# Tools
+
+The tool system enables agents to perform actions beyond text generation -- calculations, memory lookups, file reading, and sub-model calls. Tools follow a spec-driven design with a central dispatch engine and OpenAI function-calling format support.
+
+## Architecture
+
+```
+Agent --> Engine (with tool defs) --> tool_calls response --> ToolExecutor --> Tool.execute()
+ ^ |
+ | v
+ +------------------------------- ToolResult <--------------------------------------------+
+```
+
+---
+
+## BaseTool ABC
+
+All tools implement the `BaseTool` abstract base class.
+
+```python
+from abc import ABC, abstractmethod
+from openjarvis.tools._stubs import ToolSpec
+from openjarvis.core.types import ToolResult
+
+class BaseTool(ABC):
+ tool_id: str
+
+ @property
+ @abstractmethod
+ def spec(self) -> ToolSpec:
+ """Return the tool specification."""
+
+ @abstractmethod
+ def execute(self, **params) -> ToolResult:
+ """Execute the tool with the given parameters."""
+
+ def to_openai_function(self) -> dict:
+ """Convert to OpenAI function-calling format."""
+```
+
+The `to_openai_function()` method is provided by the base class and converts the tool's spec into the format expected by OpenAI-compatible APIs:
+
+```json
+{
+ "type": "function",
+ "function": {
+ "name": "calculator",
+ "description": "Evaluate a mathematical expression safely.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "expression": {
+ "type": "string",
+ "description": "Math expression to evaluate"
+ }
+ },
+ "required": ["expression"]
+ }
+ }
+}
+```
+
+---
+
+## ToolSpec
+
+The `ToolSpec` dataclass describes a tool's interface and characteristics.
+
+| Field | Type | Default | Description |
+|------------------------|------------------|---------|----------------------------------------------------|
+| `name` | `str` | -- | Unique tool identifier |
+| `description` | `str` | -- | Human-readable description (sent to the model) |
+| `parameters` | `dict[str, Any]` | `{}` | JSON Schema for the tool's parameters |
+| `category` | `str` | `""` | Tool category (e.g., `math`, `memory`, `reasoning`) |
+| `cost_estimate` | `float` | `0.0` | Estimated cost per invocation |
+| `latency_estimate` | `float` | `0.0` | Estimated latency per invocation |
+| `requires_confirmation`| `bool` | `False` | Whether the tool requires user confirmation |
+| `metadata` | `dict[str, Any]` | `{}` | Additional metadata |
+
+---
+
+## ToolResult
+
+The `ToolResult` dataclass holds the result of a tool execution.
+
+| Field | Type | Default | Description |
+|-------------------|------------------|---------|------------------------------------------|
+| `tool_name` | `str` | -- | Name of the tool that was called |
+| `content` | `str` | -- | The tool's output (text) |
+| `success` | `bool` | `True` | Whether the execution succeeded |
+| `usage` | `dict[str, Any]` | `{}` | Token usage (for LLM tool) |
+| `cost_usd` | `float` | `0.0` | Actual cost of the invocation |
+| `latency_seconds` | `float` | `0.0` | Measured execution latency |
+| `metadata` | `dict[str, Any]` | `{}` | Additional metadata |
+
+---
+
+## ToolExecutor
+
+The `ToolExecutor` is the central dispatch engine for tool calls. It manages a set of tool instances, parses JSON arguments, measures execution latency, and publishes events on the event bus.
+
+```python
+from openjarvis.tools._stubs import ToolExecutor
+
+executor = ToolExecutor(tools=[calculator, think_tool], bus=event_bus)
+
+# Get OpenAI-format tool definitions
+openai_tools = executor.get_openai_tools()
+
+# Execute a tool call
+from openjarvis.core.types import ToolCall
+tc = ToolCall(id="call_1", name="calculator", arguments='{"expression": "2+2"}')
+result = executor.execute(tc)
+print(result.content) # "4"
+```
+
+### Execution Flow
+
+1. **Parse arguments:** The `arguments` JSON string from the `ToolCall` is deserialized.
+2. **Publish start event:** `TOOL_CALL_START` is emitted on the event bus with tool name and arguments.
+3. **Execute:** The tool's `execute()` method is called with the parsed parameters.
+4. **Measure latency:** Execution time is recorded in `result.latency_seconds`.
+5. **Publish end event:** `TOOL_CALL_END` is emitted with success status and latency.
+6. **Return result:** The `ToolResult` is returned to the caller.
+
+If the tool name is unknown, a `ToolResult` with `success=False` is returned. If JSON parsing fails or the tool raises an exception, the error is captured and returned as a failed `ToolResult`.
+
+### Methods
+
+| Method | Returns | Description |
+|---------------------|------------------------|--------------------------------------------|
+| `execute(tool_call)`| `ToolResult` | Parse args, dispatch, measure, emit events |
+| `available_tools()` | `list[ToolSpec]` | Return specs for all registered tools |
+| `get_openai_tools()`| `list[dict]` | Return tools in OpenAI function format |
+
+---
+
+## Built-in Tools
+
+### Calculator
+
+**Registry key:** `calculator` | **Category:** `math`
+
+Evaluates mathematical expressions safely using Python's `ast` module. No arbitrary code execution -- only whitelisted operations are allowed.
+
+**Parameters:**
+
+| Parameter | Type | Required | Description |
+|--------------|--------|----------|------------------------------------------|
+| `expression` | string | Yes | Math expression (e.g., `"2+3*4"`, `"sqrt(16)"`) |
+
+**Supported operations:**
+
+| Category | Operations |
+|--------------|---------------------------------------------------------------|
+| Arithmetic | `+`, `-`, `*`, `/`, `//` (floor div), `%` (mod), `**` (power) |
+| Functions | `abs`, `round`, `min`, `max`, `sqrt`, `log`, `log10`, `log2` |
+| Trigonometry | `sin`, `cos`, `tan` |
+| Rounding | `ceil`, `floor` |
+| Constants | `pi`, `e` |
+
+**Example:**
+
+```python
+from openjarvis.tools.calculator import CalculatorTool
+
+calc = CalculatorTool()
+result = calc.execute(expression="sqrt(144) + 3**2")
+print(result.content) # "21.0"
+print(result.success) # True
+```
+
+### Think
+
+**Registry key:** `think` | **Category:** `reasoning`
+
+A zero-cost reasoning scratchpad. The input is echoed back as the output, allowing the model to "think out loud" during a tool-calling loop. This enables chain-of-thought reasoning within the agent workflow.
+
+**Parameters:**
+
+| Parameter | Type | Required | Description |
+|-----------|--------|----------|------------------------------------------|
+| `thought` | string | Yes | The reasoning or thought process |
+
+**Example:**
+
+```python
+from openjarvis.tools.think import ThinkTool
+
+think = ThinkTool()
+result = think.execute(thought="Let me break this problem into steps...")
+print(result.content) # "Let me break this problem into steps..."
+print(result.success) # True
+```
+
+!!! info "Cost and Latency"
+ The Think tool has zero cost and near-zero latency, making it ideal for structured reasoning without consuming additional resources.
+
+### Retrieval
+
+**Registry key:** `retrieval` | **Category:** `memory`
+
+Searches the memory backend for relevant context and returns formatted results with source attribution.
+
+**Parameters:**
+
+| Parameter | Type | Required | Description |
+|-----------|---------|----------|------------------------------------------|
+| `query` | string | Yes | Search query |
+| `top_k` | integer | No | Number of results (default: 5) |
+
+**Constructor parameters:**
+
+| Parameter | Type | Default | Description |
+|-----------|-----------------|---------|--------------------------------|
+| `backend` | `MemoryBackend` | `None` | Memory backend to search |
+| `top_k` | `int` | `5` | Default number of results |
+
+**Example:**
+
+```python
+from openjarvis.tools.retrieval import RetrievalTool
+from openjarvis.memory.sqlite import SQLiteMemory
+
+backend = SQLiteMemory(db_path="./memory.db")
+retrieval = RetrievalTool(backend=backend)
+result = retrieval.execute(query="machine learning")
+print(result.content) # Formatted context with source tags
+```
+
+### LLM
+
+**Registry key:** `llm` | **Category:** `inference`
+
+Delegates a sub-query to an inference engine. Useful for summarization, sub-questions, or generating structured output within an agent workflow.
+
+**Parameters:**
+
+| Parameter | Type | Required | Description |
+|-----------|--------|----------|------------------------------------------|
+| `prompt` | string | Yes | The prompt to send to the model |
+| `system` | string | No | Optional system message for context |
+
+**Constructor parameters:**
+
+| Parameter | Type | Default | Description |
+|-----------|-------------------|---------|--------------------------------|
+| `engine` | `InferenceEngine` | `None` | Inference engine to use |
+| `model` | `str` | `""` | Model identifier |
+
+**Example:**
+
+```python
+from openjarvis.tools.llm_tool import LLMTool
+
+llm = LLMTool(engine=my_engine, model="qwen3:8b")
+result = llm.execute(
+ prompt="Summarize: AI is transforming industries...",
+ system="You are a concise summarizer.",
+)
+print(result.content)
+```
+
+### FileRead
+
+**Registry key:** `file_read` | **Category:** `filesystem`
+
+Reads file contents with safety validations. Supports optional directory restrictions, file size limits (1 MB max), and line count limiting.
+
+**Parameters:**
+
+| Parameter | Type | Required | Description |
+|-------------|---------|----------|------------------------------------------|
+| `path` | string | Yes | Path to the file to read |
+| `max_lines` | integer | No | Maximum lines to return (default: all) |
+
+**Constructor parameters:**
+
+| Parameter | Type | Default | Description |
+|----------------|--------------|---------|-----------------------------------------------|
+| `allowed_dirs` | `list[str]` | `None` | Restrict file access to these directories |
+
+**Safety features:**
+
+- Path validation against allowed directories (when configured)
+- Maximum file size: 1 MB
+- UTF-8 encoding required (rejects binary files)
+- Existence and file-type checks
+
+**Example:**
+
+```python
+from openjarvis.tools.file_read import FileReadTool
+
+reader = FileReadTool(allowed_dirs=["/home/user/projects"])
+result = reader.execute(path="/home/user/projects/README.md", max_lines=50)
+print(result.content)
+print(result.metadata) # {"path": "/home/user/projects/README.md", "size_bytes": 1234}
+```
+
+---
+
+## Tool Registration
+
+Tools are registered via the `@ToolRegistry.register()` decorator, making them discoverable by name at runtime.
+
+```python
+from openjarvis.core.registry import ToolRegistry
+from openjarvis.tools._stubs import BaseTool, ToolSpec
+from openjarvis.core.types import ToolResult
+
+
+@ToolRegistry.register("my_tool")
+class MyTool(BaseTool):
+ tool_id = "my_tool"
+
+ @property
+ def spec(self) -> ToolSpec:
+ return ToolSpec(
+ name="my_tool",
+ description="A custom tool that does something useful.",
+ parameters={
+ "type": "object",
+ "properties": {
+ "input": {
+ "type": "string",
+ "description": "The input to process.",
+ },
+ },
+ "required": ["input"],
+ },
+ category="custom",
+ )
+
+ def execute(self, **params) -> ToolResult:
+ value = params.get("input", "")
+ return ToolResult(
+ tool_name="my_tool",
+ content=f"Processed: {value}",
+ success=True,
+ )
+```
+
+After registration, use the tool with an agent:
+
+```bash
+jarvis ask --agent orchestrator --tools my_tool "Process this data"
+```
+
+---
+
+## Using Tools with Agents
+
+### Via CLI
+
+Tools are specified as a comma-separated list with the `--tools` flag. An agent (typically `orchestrator`) must be selected:
+
+```bash
+# Single tool
+jarvis ask --agent orchestrator --tools calculator "What is 15% of 340?"
+
+# Multiple tools
+jarvis ask --agent orchestrator --tools calculator,think "Solve: 2x + 5 = 13"
+
+# All available tools (list them)
+jarvis ask --agent orchestrator --tools calculator,think,retrieval,file_read "..."
+```
+
+### Via Python SDK
+
+Tools are passed as a list of name strings:
+
+```python
+from openjarvis import Jarvis
+
+j = Jarvis()
+
+# Use calculator and think tools
+result = j.ask_full(
+ "What is the area of a circle with radius 7?",
+ agent="orchestrator",
+ tools=["calculator", "think"],
+)
+
+for tr in result["tool_results"]:
+ print(f" {tr['tool_name']}: {tr['content']} (success={tr['success']})")
+
+j.close()
+```
+
+The SDK automatically instantiates tool objects with appropriate dependencies. For example, the `retrieval` tool receives the configured memory backend, and the `llm` tool receives the active engine and model.
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 00000000..0e8a7fa5
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,165 @@
+site_name: OpenJarvis
+site_url: https://jonsaadfalcon.github.io/OpenJarvis/
+site_description: Programming abstractions for on-device AI
+site_author: OpenJarvis Contributors
+repo_url: https://github.com/jonsaadfalcon/OpenJarvis
+repo_name: jonsaadfalcon/OpenJarvis
+edit_uri: edit/main/docs/
+
+copyright: Copyright © 2026 OpenJarvis Contributors
+
+theme:
+ name: material
+ language: en
+ features:
+ - navigation.tabs
+ - navigation.tabs.sticky
+ - navigation.sections
+ - navigation.top
+ - navigation.indexes
+ - navigation.footer
+ - search.suggest
+ - search.highlight
+ - content.code.copy
+ - content.code.annotate
+ - content.tabs.link
+ - toc.follow
+ palette:
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ primary: indigo
+ accent: amber
+ toggle:
+ icon: material/brightness-7
+ name: Switch to dark mode
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ primary: indigo
+ accent: amber
+ toggle:
+ icon: material/brightness-4
+ name: Switch to light mode
+ icon:
+ repo: fontawesome/brands/github
+
+plugins:
+ - search
+ - mkdocstrings:
+ default_handler: python
+ handlers:
+ python:
+ paths: [src]
+ options:
+ show_source: true
+ show_root_heading: true
+ show_root_full_path: false
+ show_category_heading: true
+ show_symbol_type_heading: true
+ show_symbol_type_toc: true
+ members_order: source
+ docstring_style: numpy
+ docstring_section_style: spacy
+ merge_init_into_class: true
+ show_signature_annotations: true
+ separate_signature: true
+ signature_crossrefs: true
+ show_if_no_docstring: false
+ inherited_members: false
+ filters:
+ - "!^_"
+ - "^__init__$"
+ - "^__all__$"
+
+markdown_extensions:
+ - abbr
+ - admonition
+ - attr_list
+ - def_list
+ - footnotes
+ - md_in_html
+ - tables
+ - toc:
+ permalink: true
+ toc_depth: 3
+ - pymdownx.betterem:
+ smart_enable: all
+ - pymdownx.caret
+ - pymdownx.details
+ - pymdownx.emoji:
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
+ - pymdownx.highlight:
+ anchor_linenums: true
+ line_spans: __span
+ pygments_lang_class: true
+ - pymdownx.inlinehilite
+ - pymdownx.keys
+ - pymdownx.mark
+ - pymdownx.smartsymbols
+ - pymdownx.snippets:
+ auto_append:
+ - docs/includes/abbreviations.md
+ check_paths: false
+ - pymdownx.superfences:
+ custom_fences:
+ - name: mermaid
+ class: mermaid
+ format: !!python/name:pymdownx.superfences.fence_code_format
+ - pymdownx.tabbed:
+ alternate_style: true
+ - pymdownx.tasklist:
+ custom_checkbox: true
+ - pymdownx.tilde
+
+extra:
+ social:
+ - icon: fontawesome/brands/github
+ link: https://github.com/jonsaadfalcon/OpenJarvis
+
+nav:
+ - Home: index.md
+ - Getting Started:
+ - Installation: getting-started/installation.md
+ - Quick Start: getting-started/quickstart.md
+ - Configuration: getting-started/configuration.md
+ - User Guide:
+ - CLI Reference: user-guide/cli.md
+ - Python SDK: user-guide/python-sdk.md
+ - Agents: user-guide/agents.md
+ - Memory: user-guide/memory.md
+ - Tools: user-guide/tools.md
+ - Telemetry & Traces: user-guide/telemetry.md
+ - Benchmarks: user-guide/benchmarks.md
+ - Architecture:
+ - Overview: architecture/overview.md
+ - Intelligence: architecture/intelligence.md
+ - Inference Engine: architecture/engine.md
+ - Agentic Logic: architecture/agents.md
+ - Memory & Storage: architecture/memory.md
+ - Learning & Traces: architecture/learning.md
+ - Query Flow: architecture/query-flow.md
+ - Design Principles: architecture/design-principles.md
+ - API Reference:
+ - api/index.md
+ - SDK (Jarvis): api/sdk.md
+ - Core: api/core.md
+ - Engine: api/engine.md
+ - Agents: api/agents.md
+ - Memory: api/memory.md
+ - Tools: api/tools.md
+ - Intelligence: api/intelligence.md
+ - Learning: api/learning.md
+ - Traces: api/traces.md
+ - Telemetry: api/telemetry.md
+ - Benchmarks: api/bench.md
+ - Server: api/server.md
+ - Deployment:
+ - Docker: deployment/docker.md
+ - systemd (Linux): deployment/systemd.md
+ - launchd (macOS): deployment/launchd.md
+ - API Server: deployment/api-server.md
+ - Development:
+ - Contributing: development/contributing.md
+ - Extending OpenJarvis: development/extending.md
+ - Roadmap: development/roadmap.md
+ - Changelog: development/changelog.md
diff --git a/pyproject.toml b/pyproject.toml
index 39a16867..8a75153d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -55,6 +55,11 @@ server = [
agents = []
learning = []
openclaw = []
+docs = [
+ "mkdocs>=1.6",
+ "mkdocs-material>=9.5",
+ "mkdocstrings[python]>=0.25",
+]
[project.scripts]
jarvis = "openjarvis.cli:main"