Architecture
System Overview
nanobot is an event-driven, single-process AI assistant built around an async message bus. Chat channels produce inbound messages, the agent loop consumes them, calls LLMs with tool definitions, executes tool calls, and sends responses back through channels. The entire system runs in a single Python asyncio event loop.
High-Level Architecture
graph TB
subgraph "Chat Channels"
Telegram[Telegram]
Discord[Discord]
Slack[Slack]
WhatsApp[WhatsApp]
Feishu[Feishu]
DingTalk[DingTalk]
QQ[QQ]
Email[Email]
Matrix[Matrix]
Mochat[Mochat]
end
subgraph "Message Bus"
InQ[Inbound Queue]
OutQ[Outbound Queue]
end
subgraph "Agent Core"
Loop[Agent Loop]
Context[Context Builder]
Memory[Memory Store]
Skills[Skills Loader]
Tools[Tool Registry]
Session[Session Manager]
end
subgraph "Tools"
FS[File System]
Shell[Shell Exec]
Web[Web Search/Fetch]
Cron[Cron Scheduler]
Spawn[Subagent Spawn]
MCP[MCP Bridge]
Msg[Message Tool]
end
subgraph "LLM Providers"
LiteLLM[LiteLLM Provider]
Custom[Custom Provider]
Codex[Codex Provider]
end
subgraph "External LLMs"
OpenRouter[OpenRouter]
Anthropic[Anthropic]
OpenAI[OpenAI]
DeepSeek[DeepSeek]
Gemini[Gemini]
Others[Others...]
end
Telegram & Discord & Slack & WhatsApp & Feishu & DingTalk & QQ & Email & Matrix & Mochat --> InQ
InQ --> Loop
Loop --> Context
Context --> Memory
Context --> Skills
Loop --> Session
Loop --> Tools
Tools --> FS & Shell & Web & Cron & Spawn & MCP & Msg
Loop --> LiteLLM & Custom & Codex
LiteLLM --> OpenRouter & Anthropic & OpenAI & DeepSeek & Gemini & Others
Loop --> OutQ
OutQ --> Telegram & Discord & Slack & WhatsApp & Feishu & DingTalk & QQ & Email & Matrix & Mochat
style Loop fill:#e8f5e9
style InQ fill:#e3f2fd
style OutQ fill:#e3f2fd
Component Architecture
1. Message Bus (nanobot/bus/)
The MessageBus is the central decoupling mechanism. It uses two asyncio.Queue instances:
Inbound queue: Channels push
InboundMessage→ Agent consumesOutbound queue: Agent pushes
OutboundMessage→ ChannelManager dispatches to the correct channel
Key design decision: The bus is fully async and non-blocking. Channels and the agent loop run as independent asyncio tasks, communicating only through queues.
Files:
nanobot/bus/queue.py:MessageBus— Queue wrappernanobot/bus/events.py—InboundMessage,OutboundMessagedataclasses
2. Channels (nanobot/channels/)
Each channel is a subclass of BaseChannel (nanobot/channels/base.py) and implements three methods:
start()— Connect to the platform and listen for messagesstop()— Disconnect and clean upsend(msg)— Deliver an outbound message
The ChannelManager (nanobot/channels/manager.py) initializes all enabled channels from config, starts them as asyncio tasks, and dispatches outbound messages by matching msg.channel to the correct channel instance.
Access control: Each channel enforces an allowFrom whitelist. The is_allowed(sender_id) method in BaseChannel checks the whitelist before forwarding messages to the bus.
Channel |
Protocol |
Auth Method |
|---|---|---|
Telegram |
HTTP long-poll |
Bot token |
Discord |
WebSocket gateway |
Bot token |
Slack |
Socket Mode (WebSocket) |
Bot + App tokens |
WebSocket (Node.js bridge) |
QR code scan |
|
Feishu |
WebSocket long connection |
App ID + Secret |
DingTalk |
Stream Mode |
Client ID + Secret |
WebSocket (botpy) |
App ID + Secret |
|
IMAP poll + SMTP send |
Username + Password |
|
Matrix |
Matrix sync API |
Access token |
Mochat |
Socket.IO |
Claw token |
3. Agent Loop (nanobot/agent/loop.py)
The AgentLoop class is the core processing engine. Its main loop:
Consume an
InboundMessagefrom the busLoad/create a
Sessionkeyed bychannel:chat_idBuild context via
ContextBuilder— assembles the system prompt from identity, bootstrap files (AGENTS.md,SOUL.md,USER.md, etc.), memory, and skillsCall LLM via
LLMProvider.chat()with message history and tool schemasExecute tools if the
LLMResponsecontainstool_calls— dispatch toToolRegistryLoop back to step 4 if tool results need further LLM processing
Publish the final
OutboundMessageto the bus
sequenceDiagram
participant Channel
participant Bus
participant AgentLoop
participant Context
participant LLM
participant ToolRegistry
participant Session
Channel->>Bus: InboundMessage
Bus->>AgentLoop: consume_inbound()
AgentLoop->>Session: get/create session
AgentLoop->>Context: build_system_prompt()
Context->>Context: identity + bootstrap + memory + skills
AgentLoop->>Session: get_history()
loop Until no tool_calls
AgentLoop->>LLM: chat(messages, tools)
LLM-->>AgentLoop: LLMResponse
alt Has tool_calls
AgentLoop->>ToolRegistry: execute tool
ToolRegistry-->>AgentLoop: tool result
AgentLoop->>Session: append tool result
end
end
AgentLoop->>Session: save_turn()
AgentLoop->>Bus: OutboundMessage
Bus->>Channel: send()
4. Context Builder (nanobot/agent/context.py)
The ContextBuilder assembles the system prompt by concatenating:
Identity — A built-in persona description
Bootstrap files — User-customizable files in the workspace:
AGENTS.md,SOUL.md,USER.md,TOOLS.md,IDENTITY.mdMemory — Consolidated memory from
MemoryStoreAlways-on skills — Skills configured to always load
Skills summary — Brief descriptions of available skills so the LLM knows what it can activate via
read_fileRuntime context — Current date/time, platform info
5. Provider System (nanobot/providers/)
The provider system uses a Registry pattern (nanobot/providers/registry.py):
ProviderSpecis a frozen dataclass describing each provider’s metadata (name, keywords, env vars, LiteLLM prefix, etc.)The
PROVIDERStuple is the single source of truth — ordering controls match priorityThree provider implementations:
LiteLLMProvider— Routes through LiteLLM for broad model supportCustomProvider— Direct OpenAI-compatible HTTP calls (bypasses LiteLLM)OpenAICodexProvider— OAuth-based authentication flow
classDiagram
class LLMProvider {
<<abstract>>
+chat(messages, tools, model, ...) LLMResponse
+get_default_model() str
}
class LiteLLMProvider {
+chat()
+get_default_model()
}
class CustomProvider {
+chat()
+get_default_model()
}
class OpenAICodexProvider {
+chat()
+get_default_model()
}
class ProviderSpec {
+name: str
+keywords: tuple
+env_key: str
+litellm_prefix: str
+is_gateway: bool
+is_direct: bool
}
LLMProvider <|-- LiteLLMProvider
LLMProvider <|-- CustomProvider
LLMProvider <|-- OpenAICodexProvider
6. Tool System (nanobot/agent/tools/)
All tools inherit from the Tool ABC (nanobot/agent/tools/base.py):
Tool |
File |
Description |
|---|---|---|
|
|
Read file contents |
|
|
Write/create files |
|
|
Patch files with search/replace |
|
|
List directory contents |
|
|
Execute shell commands |
|
|
Search the web (Brave API) |
|
|
Fetch and extract web page content |
|
|
Send a message back to the user |
|
|
Launch a background subagent |
|
|
Manage scheduled tasks |
MCP tools |
|
Dynamic tools from MCP servers |
The ToolRegistry (nanobot/agent/tools/registry.py) collects all tool instances and provides:
get_schemas()— Returns OpenAI-format function schemas for the LLMexecute(name, params)— Dispatches to the correct tool
Tool parameters are validated against JSON Schema before execution via Tool.validate_params().
7. Session Management (nanobot/session/)
Sessions are keyed by channel:chat_id. Each session:
Stores messages as a
list[dict](append-only for LLM cache efficiency)Persists to disk as JSONL files in
~/.nanobot/sessions/Supports history consolidation: after a threshold, older messages are summarized into
HISTORY.mdandMEMORY.mdReturns only unconsolidated messages via
get_history(), aligned to user turns to avoid orphaned tool results
8. Skills System (nanobot/agent/skills.py)
Skills are Markdown-based capability descriptions stored as SKILL.md files:
Bundled skills:
nanobot/skills/(weather, github, tmux, cron, etc.)User skills:
~/.nanobot/workspace/skills/
The SkillsLoader scans both directories, builds a summary for the system prompt, and loads full skill content on demand when the LLM reads the SKILL.md file via the read_file tool.
9. Heartbeat & Cron
HeartbeatService (
nanobot/heartbeat/service.py): Wakes up every 30 minutes, reads~/.nanobot/workspace/HEARTBEAT.md, and asks the LLM whether tasks should be executedCronService (
nanobot/cron/service.py): Usescroniterfor precise cron-expression scheduling; thecrontool lets the LLM manage scheduled tasks
Cross-Cutting Concerns
Error Handling
The agent loop catches exceptions during tool execution and returns error messages to the LLM as tool results, allowing the LLM to recover or inform the user. Provider errors (API failures, rate limits) are logged and propagated as user-facing error messages.
Logging
nanobot uses loguru for all logging. Logs go to stderr by default; the --logs flag in CLI mode shows them alongside chat output.
Security
Workspace sandboxing:
tools.restrictToWorkspacelimits all file/shell tools to the workspace directoryChannel access control:
allowFromper channel, checked inBaseChannel.is_allowed()Session isolation: Sessions are keyed by
channel:chat_id— no cross-conversation leakageMCP tool timeout: Configurable
toolTimeout(default 30s) prevents hung MCP servers from blocking the agent
Configuration
Single Config Pydantic model (nanobot/config/schema.py) validates all configuration:
Accepts both camelCase and snake_case keys (via
alias_generator=to_camel)Nested models:
ProvidersConfig,ChannelsConfig,AgentsConfig,ToolsConfigLoaded from
~/.nanobot/config.jsonwith sensible defaults