Architecture¶
MiniBot is an asyncio application with two runtime entrypoints: daemon mode
(minibot.app.daemon) for Telegram, and interactive CLI mode
(minibot.app.console) for local console conversations. Both publish inbound events
to an internal event bus, route messages through the LLM pipeline, and emit outbound
responses back to the active channel adapter. A third, deliberately narrower
entrypoint is the forked task worker (minibot.app.tasks.worker).
Guiding principles¶
Lightweight hexagonal split —
core(domain contracts),app(orchestration),adapters(infrastructure),llm(provider/tool integration).Async-first boundaries for I/O-heavy paths (Telegram, DB, provider calls).
Replaceable infrastructure behind protocols (memory repositories, scheduled prompt store, tools, vector stores).
Explicit, testable flow with dependency wiring centralized in the composition roots (
minibot.app.daemonandminibot.app.console), which hand concrete adapters to the dispatcher instead of letting it resolve them through the container.Lean core — if a feature works as an extension, it ships as one. See Lean core.
Lean core¶
The core is the part of MiniBot that cannot be expressed through the extension API. Everything
else, including the tools and services MiniBot ships with, is a bundled register(mb) module
under minibot/extensions/, built on the same Extension API Reference a third-party extension uses.
The rule follows PostgreSQL’s: keep the core small, and make it generic enough that extensions
are first-class.
Extension first. A tool, service, channel, page or prompt fragment that can be built from
ExtensionContextbelongs in a bundled extension, not inbuild_enabled_tools(minibot.app.tool_factory).What stays in core. Tools that need state the extension API does not expose (the conversation memory backend, the skill registry, managed file storage, the config path), and the wrappers every tool passes through (approval, output spill, tool-call events).
Generalize instead of special-casing. When a feature needs something
ExtensionContextlacks, prefer widening the extension API over adding a core-only branch, so the next extension can use it too.Extensions are first-class. Core mechanisms that act on tools — main-agent visibility, approval patterns, output spill, the task worker’s allowlist — must treat an extension tool exactly like a core one. A core feature that only works with core tools is incomplete.
Repository layout¶
Directories only; module-level detail lives in the linked pages below. Dependencies
point inward — adapters may import core and config; core imports none of
app, adapters, llm or config; llm never imports app or adapters; app
imports adapters only in the composition roots (daemon, console) and the task
worker.
minibot/
app/ orchestration: daemon, dispatcher, handler, agent runtime, tool factory,
extensions API
handlers/services/ LLM turn collaborators
tasks/ task manager and the forked worker entrypoint
config/ settings schema, `${ENV_VAR}` expansion, config path resolution
core/ domain models + protocols (channels, events, files, tasks, MCP, memory)
adapters/ infrastructure: config loading, container, logging, http, memory, graph,
vectors, qdrant, messaging, scheduler, tasks (stores), files, mcp, vault
llm/ provider factory + tool schemas/handlers (providers/, services/, tools/)
extensions/ bundled extensions: thin register(mb) composition
rag/ ingestion, chunking, embeddings, retrieval
shared/ small cross-cutting helpers
Runtime flow¶
Entry point (
minibot.app.daemonorminibot.app.console) boots settings, logging, memory, tools, and dispatcher. The daemon first replays turns marked pending by a previous crash, then optionally starts the built-in HTTP server.A channel adapter maps input into
ChannelMessageand publishesMessageEvent.app.event_bus.EventBusdelivers the event toapp.dispatcher.Dispatcher.The dispatcher builds main-agent tool visibility (allow/deny policy, exclusive ownership mode) and invokes
LLMMessageHandler.The handler loads history, composes system prompt fragments, and builds tool context.
The main agent (
minibot) runs its tool loop inAgentRuntime.Delegation is tool-driven and asynchronous:
delegateorspawn_taskenqueues the work, the task consumer leases it, and a worker subprocess applies the specialist’s tool policy and runs it. Its answer is published later as its ownOutboundEvent.The handler returns
ChannelResponsewith metadata (primary_agent, token trace).The dispatcher publishes
OutboundEvent; the active channel renders it to the user. If the handler raises, the dispatcher publishesTurnFailedEventand a short generic failure reply, never the exception text.
The dispatcher awaits each turn before taking the next message, so turns run strictly one at a
time. One slow turn, or a tool approval wait (up to [tools.approval].timeout_seconds), queues
every message behind it. That fits a single owner; delegated tasks run in their own worker
processes and do not hold the queue.
Console agent invocation¶
flowchart TD
U[User in terminal] --> C[minibot console]
C --> CS[ConsoleService.publish_user_message]
CS --> EB[(EventBus)]
EB --> D[Dispatcher]
D --> H[LLMMessageHandler.handle]
H --> SUP[Main Agent Runtime]
SUP --> DEC{Need specialist?}
DEC -->|no| FINAL[Main agent final answer]
DEC -->|yes| ST[delegate / spawn_task tool]
ST --> Q[(Task queue)]
ST --> ACK[task_id acknowledged, turn ends]
Q --> CON[Task consumer leases]
CON --> W[Worker subprocess: AgentRegistry lookup + specialist runtime]
W --> RES[TaskManager publishes result]
ACK --> RESP[ChannelResponse + metadata]
FINAL --> RESP
RESP --> EB2[(OutboundEvent)]
RES --> EB2
EB2 --> C
C --> U2[Rendered response in terminal]
Layer map¶
config— theSettingsschema, environment expansion and config path resolution, shared by every layer.core— domain contracts:AgentSpec, runtime state, channel DTOs andChannelCapabilities, events, file/MCP/memory/graph/task/vector protocols.app— event bus, dispatcher,LLMMessageHandlerand its services, agent runtime/registry/policies, tool factory, extension API, guardrails, scheduler and task services (app.tasks: manager and worker).adapters— config loading, container, logging, the optional HTTP server, SQLite/SQLAlchemy memory, Telegram/console/RabbitMQ messaging (Telegram supplies its ownChannelCapabilities), scheduler persistence, task stores, local files, MCP clients, credential vault.llm— provider factory and services (request building, schema policy, tool execution, usage parsing, compaction), plus the LLM-facing tool schemas/handlers.extensions— bundled, opt-inregister(mb)modules (tools, channels, integrations, services).rag— document ingestion, chunking, embeddings, reranking, retrieval.shared— generic helpers (prompt loading, frontmatter, paths, retries).
Data and state¶
Conversation history: SQLite transcript store (optional trimming/compaction), with an FTS5 index over message text for the web UI’s search.
Pending turns: SQLite rows marking in-flight turns, replayed by the daemon after a crash.
KV notes: optional SQLAlchemy-backed store under tool controls.
Scheduled prompts: SQLite prompt store with recurrence + retry metadata.
Vector index: whichever
VectorStorebackend[tools.rag]selects.Credentials: encrypted
secrets.vault.yml, decrypted into process memory only.
Where to go next¶
Extending MiniBot: Extending MiniBot, Writing extensions, Extension API Reference, Events
Agents and prompts: Multi-Agent Orchestration, Prompt Packs
Tools and integrations: Agent Tools, MCP Integration, Async Tasks, Scheduled Tasks & Automation, Relation graph
Providers: Providers
Retrieval and media: RAG & Persistent Knowledge, Audio Transcription
Operations: Configuration Reference, CLI Reference, Security & Sandboxing, Credential Vault