How a 4,000‑Line NanoBot Architecture Enables a Controllable AI Agent

NanoBot demonstrates that a full‑featured, controllable AI agent can be built with roughly 4,000 lines of code by using a minimal runtime consisting of a MessageBus, an AgentLoop, a file‑based ContextBuilder, a registered ToolRegistry with JSON‑Schema validation, and lightweight Cron/Heartbeat mechanisms, offering a clear contrast to heavier frameworks like OpenClaw.

DeepNoMind
DeepNoMind
DeepNoMind
How a 4,000‑Line NanoBot Architecture Enables a Controllable AI Agent

Why it matters: controllability first

Many developers want a usable agent but fear a black‑box where they cannot see which tool is called, which files are modified, or why a response deviates. NanoBot addresses this by constructing the agent as the smallest viable runtime, keeping the entire pipeline readable, debuggable, and extensible.

Core design goals

Read the whole end‑to‑end pipeline (codebase small enough to read entirely).

Precisely locate where a problem occurs (clear boundaries).

Safely add capabilities incrementally (components are replaceable).

Minimal runtime architecture

The agent processes messages through a fixed sequence: Message → Context assembly → LLM decision → Tool execution → Result back‑fill → Response. This is driven by three main components:

Channels : unify inbound messages from Telegram, WhatsApp, Slack, etc. into InboundMessage objects.

MessageBus : decouples message receipt from response generation with two asyncio queues ( inbound and outbound).

AgentLoop : the core loop that pulls a message, builds a system prompt via ContextBuilder, calls the LLM, executes any returned tool calls, feeds tool results back, and repeats until the model stops invoking tools.

AgentLoop step‑by‑step

Pull a message from the inbound queue.

Load or create a session and fetch the latest N historic messages.

Use ContextBuilder to assemble system prompts from the files AGENTS.md, SOUL.md, USER.md, TOOLS.md, IDENTITY.md and optional memory files.

Call the LLM, passing the tool schema as function definitions.

If the model returns a tool call, execute each tool, convert the result into a tool‑role message, and feed it back to the LLM for the next reasoning step.

If the model stops calling tools, write the final content to the session and enqueue the outbound response.

This separation of "thinking" (LLM) and "doing" (tools) guarantees that the model never imagines execution results; it always works with real tool output.

MessageBus rationale

Mixing receipt and processing leads to format mismatches (voice in Telegram, media in WhatsApp, mentions in Slack) and makes serialisation hard. By inserting a queue layer, Channels need not know the agent internals, the AgentLoop need not know the source, and observability, rate‑limiting, or priority can be added simply at the queue level.

Sub‑agents (spawn)

When a task is too long or complex, the main agent can spawn a child agent that works on an isolated sub‑task (e.g., reading a batch of files and returning a structured summary). The child returns its result via the MessageBus, and the parent composes the final answer.

Gateway command

Running nanobot gateway launches four components in one process: AgentLoop, ChannelManager (starts Telegram/WhatsApp/Slack adapters), CronService (reads ~/.nanobot/cron/jobs.json and injects timed messages), and HeartbeatService (wakes the agent every 30 minutes to read HEARTBEAT.md).

ContextBuilder and file‑based prompts

All configuration lives in version‑controlled markdown files. ContextBuilder.build_system_prompt loads these files, appends optional memory snippets, and joins them with separators. This makes rules auditable, editable, and independent of the chat history.

SkillsLoader (progressive skill loading)

Only skills marked always=true are inserted directly into the system prompt. Other skills are presented as name/description/path summaries; the agent reads the full definition on demand via read_file. This prevents the prompt from exploding when many skills exist.

MemoryStore

Memory consists of daily notes ( memory/YYYY‑MM‑DD.md) and a manually maintained long‑term file ( memory/MEMORY.md). The store returns a combined string for the prompt. It is simple and readable but lacks vector‑based retrieval; the author notes this as future work.

Tool system

Tools are registered in a ToolRegistry, which packages each tool’s name, description, parameters, and execution logic into a JSON schema for the LLM. Parameter validation is performed by validate_params(), converting errors into readable text that the model can correct. This prevents malformed tool calls from crashing the loop.

Example: web_fetch

The web_fetch tool returns a JSON string containing finalUrl, status, extractor, truncated, and text. The structured output lets the agent know whether the fetch succeeded, was truncated, or redirected, which is essential for reliable reasoning.

LiteLLM provider (multi‑model routing)

NanoBot uses a thin wrapper around LiteLLM to route requests to OpenRouter, Anthropic, OpenAI, DeepSeek, Gemini, Groq, or a local vLLM instance. Configuration is a simple JSON block specifying API keys and defaults, e.g., "model": "anthropic/claude-opus-4-5" for OpenRouter or a local endpoint for vLLM.

Proactive execution: Cron & Heartbeat

Cron : explicit scheduled jobs that inject a synthetic user message into the agent pipeline at the configured time.

Heartbeat : a lightweight timer that every 30 minutes reads HEARTBEAT.md; if the file contains tasks, they are executed, otherwise nothing happens.

Both mechanisms turn “always‑on” behavior into a cheap, testable input rather than a complex workflow DSL.

Channels and boundary enforcement

Channels abstract away protocol specifics (long‑polling for Telegram, Node bridge for WhatsApp, WebSocket for Slack) and produce uniform InboundMessage / OutboundMessage objects. The allowFrom field can restrict which senders are permitted, preventing accidental responses to unknown sources.

Comparison with OpenClaw

OpenClaw is a production‑ready product; NanoBot is a minimal skeleton for learning and replication. The author notes a clear difference: OpenClaw ships as a ready‑to‑run bot, while NanoBot provides the bare‑bones runtime that can be extended step‑by‑step.

TL;DR (key takeaways)

Agent reduced to the loop: message → context → LLM → tool → back‑fill → response.

MessageBus decouples channels from the core loop.

AgentLoop implements a textbook tool‑call cycle.

ContextBuilder assembles prompts from version‑controlled markdown files.

ToolRegistry registers tools and validates parameters with JSON Schema. exec tool has coarse safety (regex guard, optional workspace limit) but needs allow‑list and audit for production.

Sessions stored as JSONL in ~/.nanobot/sessions, not in the project workspace.

Cron/Heartbeat give the agent proactive capability.

Actual code size is ~3.5 k lines (core) with startup time 0.8 s and memory ~45 MB, far lower than OpenClaw.

Metrics and community

GitHub: 15.9 k stars, 2.2 k forks, 32+ contributors.

Latest release v0.1.3.post6 (2026‑02‑10).

Supported channels: Telegram, Discord, WhatsApp, Mochat, DingTalk, Slack, Email, QQ.

Supported LLM providers: 13+, including OpenRouter, Anthropic, OpenAI, DeepSeek, Gemini, Groq, vLLM.

What to watch out for

MemoryStore is a simple notebook; a searchable long‑term store is still missing.

Exec tool’s regex guard is insufficient for production; an allow‑list, confirmation step, and audit log are recommended.

Session files live in ~/.nanobot/sessions, which complicates moving the workspace to another machine.

Current implementation processes tool calls sequentially; parallel orchestration, retry policies, and advanced concurrency need further work.

The function process_direct() ignores session_key, causing system‑triggered inputs (Cron/Heartbeat) to clash with interactive CLI sessions.

Reading order for the codebase

Start with nanobot/agent/loop.py to grasp the main pipeline.

Then inspect nanobot/agent/context.py for prompt assembly.

Next, explore nanobot/agent/tools/* to understand the tool system and safety checks.

Review nanobot/cron/* and nanobot/heartbeat/* for proactive behaviour.

Finally, look at nanobot/channels/* when adding new entry points.

The architecture shows that an effective AI agent does not need a massive codebase; a well‑designed minimal skeleton can deliver full functionality while remaining fast, lightweight, and highly controllable.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AI AgentCronHeartbeatMessageBusNanoBotAgentLoopContextBuilderToolRegistry
DeepNoMind
Written by

DeepNoMind

I’m Yu Fan, a tech leader with deep technical expertise and managerial vision. Formerly at Motorola, now at Mavenir, I’ve led teams for years, focusing on backend architecture and cloud-native solutions, staying abreast of AI and other frontier fields, and championing personal growth and lifelong learning.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.