Architecture
This page is a tour of how OpenAgent is put together — the long-lived components, how a message moves through them, and where state lives. Diagrams use Mermaid: plain text, renders automatically on GitHub and in most Markdown viewers, easy for humans and AI assistants to edit.
The Gateway — the WebSocket + REST surface that clients connect to — is covered in its own Gateway page and is drawn here only as the transport boundary.
1. Component map
Everything below runs inside a single AgentServer process started by openagent serve. The server owns the lifecycle of the agent, MCP pool, scheduler, and any bridges; nothing runs as a separate daemon.
Each box expands into its own section below. The later diagrams zoom in on what SmartRouter does (§3), how the MCP pool is built (§4), how the vault is accessed (§5), how the scheduler drives tasks and Dream Mode (§6), and where state lives (§8). The Gateway itself has its own dedicated page.
2. Message flow
A chat turn arrives at the Gateway, gets queued per client, and lands in Agent.run(). The agent hands generation to SmartRouter, which either delegates to Agno (which runs its own tool loop against the pool) or to Claude CLI (which spawns a subprocess with the same MCP pool wired in).
3. SmartRouter: one router, two backends
OpenAgent is model-agnostic because SmartRouter is the only thing the agent talks to. It owns three responsibilities on every turn:
- Read the enabled catalog. Rows in the
modelstable markedenabled=1, joined withproviders, produce the set ofruntime_ids the router may dispatch to. Zero enabled models → fail-fast error, no silent fallback. - Pick a model. If the session is already bound, reuse that binding. Otherwise a cheap classifier LLM picks the single best
runtime_idfrom the enabled catalog based on the turn's content. - Bind the session. First dispatch writes
session_bindingsso every follow-up turn in that session stays on the same side (Agno'sSqliteDbhistory vs. Claude CLI's session store — mixing them would split the conversation). Bindings persist across restarts viasession_bindingsandsdk_sessions.
Agno side
AgnoProvider wraps Agno's Agent, which runs the tool-calling loop internally. It consumes the pool's pre-built MCPTools toolkits — so OpenAgent doesn't reimplement tool dispatch, retries, or JSON-schema plumbing. Per-session history is stored in Agno's SqliteDb.
Claude CLI side
ClaudeCLIRegistry manages one or more claude-cli models (e.g. claude-cli/claude-sonnet-4-6). On first dispatch for a session it spawns claude-agent-sdk as a subprocess, passing the pool's stdio/URL specs to ClaudeSDKClient(mcp_servers=…) and --strict-mcp-config so MCP load failures surface immediately. Session IDs are mapped in sdk_sessions so subsequent turns resume the same conversation.
Hot reload
Edit a model or provider via the manager MCPs, REST, or the UI — the gateway checks updated_at before the next turn and rebuilds the routing table in place. Bound sessions keep their binding; new sessions can land on the new entry.
4. MCP Pool: built-ins + customs, shared by both backends
MCPPool is the single source of truth for tools available to the agent. Both model sides (Agno and Claude CLI) read from the same pool, so we don't pay N times to spin up the same subprocess when the router dispatches between tiers.
Built-ins vs custom. Built-in rows (kind='default' or 'builtin') are auto-seeded on every boot from BUILTIN_MCP_SPECS — missing rows are reinstated, existing ones (even disabled) are left untouched. Built-ins cannot be removed, only disabled. Custom rows (kind='custom') are full CRUD via mcp-manager, POST /api/mcps, or the MCPs UI tab.
Tool naming. Tools are namespaced <server>_<tool> (filesystem_read_text_file, vault_write_note, scheduler_create_scheduled_task) so servers never collide.
See MCP Tools for the built-in matrix and custom-MCP recipes.
5. Memory vault: markdown, not a database
Long-term memory is a plain Obsidian-compatible markdown vault — one .md file per note, YAML frontmatter, [[wikilinks]], tags. The agent never talks to it directly; every read and write goes through the vault MCP, which is just another server in the pool.
The same folder opens untouched in Obsidian — graph view, backlinks, plugins all work. The REST endpoints under /api/vault/* give the desktop app a read surface (notes list, graph, full-text search) without going through the MCP round-trip. See Memory & Vault for note conventions.
Only scheduled-task and bookkeeping state lives in the SQLite DB — the vault is the knowledge store.
6. Scheduler and Dream Mode
The Scheduler is a 30-second tick loop that reads scheduled_tasks from SQLite and invokes agent.run(prompt) for each task whose cron is due. Because tasks call the regular agent entry point, they get the same model router, MCP pool, and vault access as any user turn — a task is just a prompt on a schedule.
Dream Mode is a specific built-in scheduled task that runs nightly maintenance: consolidates duplicate memory files, cross-links notes with wikilinks, runs a health check, writes a dream log back to the vault. It has no dedicated daemon — it's literally a scheduled_tasks row with a fixed prompt and a nightly cron, invoked through the same tick loop.
dream_mode:
enabled: true
time: "3:00" # local timeAuto-update piggybacks on the same tick (default every 6 hours): check GitHub releases → download → on next restart the launcher picks the new binary. See Scheduler & Dream Mode for CLI management (openagent task add / list / enable / disable).
7. Startup and shutdown
AgentServer.start() brings components up in a fixed order so the Gateway never accepts traffic before the agent and MCP pool are ready; shutdown runs the reverse with bounded timeouts.
8. State layout
Config is layered: CLI flags → openagent.yaml → SQLite runtime overrides. Anything a user can toggle at runtime (MCPs, models, providers, tasks, session bindings, usage) lives in the DB; the YAML is for bootstrap, channel credentials, and things that rarely change.
9. Extensibility at a glance
Three mechanisms, all configuration-driven — no plugin framework:
- MCP servers add tools (
mcp-manager,POST /api/mcps, or the MCPs UI tab). No code changes. - Scheduled tasks put
agent.run(prompt)on a cron (schedulerMCP,/api/scheduled-tasks, oropenagent task add). - Channels / bridges are WebSocket clients of the Gateway (
BaseBridgesubclass, ~150 lines) — adding a new platform never touches the core.
Editing these diagrams
Every diagram is a fenced ```mermaid block. Preview locally with the VS Code Mermaid extension, or push to GitHub and view the rendered Markdown. AI assistants can edit any block directly with an Edit on this file.