Inside Nanobot: A Deep Dive into a Lightweight AI Assistant Framework

This article provides a comprehensive walkthrough of the open‑source Nanobot project, detailing its architecture, core configuration, message bus, tool system, LLM provider, context builder, session management, agent loop, channel integration, cron and heartbeat services, and CLI commands, while illustrating each component with code snippets and diagrams.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
Inside Nanobot: A Deep Dive into a Lightweight AI Assistant Framework

Introduction

The Nanobot repository ( https://github.com/HKUDS/nanobot) provides an ultra‑lightweight personal AI assistant framework. It supports LLM chat, tool invocation, multi‑channel communication (Discord, Feishu), session memory, scheduled tasks, and background sub‑agents.

Project Skeleton

Overall Architecture

The system receives a message from a chat platform, the LLM processes the message, optionally calls tools (e.g., web search, file operations), and feeds tool results back into the LLM context. This loop repeats until the LLM produces a final reply or the task is manually interrupted. Memory and Skills provide persistent context throughout the process.

The repository layout groups functionality into packages such as bus, providers, agent, session, and channels. Package names indicate their responsibilities.

Core Configuration File

The pyproject.toml file lists a minimal set of dependencies and defines an entry point:

[project.scripts]
nanobot = "nanobot.cli.commands:app"

Installing the package creates a nanobot command that launches the application.

Infrastructure Services

Message Bus ( bus )

The bus/events.py module defines two message types: InboundMessage (user‑sent) and OutboundMessage (assistant reply). bus/queue.py implements two asynchronous queues— inbound for incoming messages and outbound for outgoing messages—plus publish/consume helpers. This decouples agents and channels so each only handles a single message format.

bus/                     # Message bus
├── events.py          # Defines InboundMessage and OutboundMessage
└── queue.py          # Async inbound/outbound queues + subscription mechanism

LLM Provider

The abstract LLMProvider defines a chat method returning an LLMResponse. The concrete LiteLLMProvider uses the liteLLM library to support dozens of models (OpenRouter, Anthropic, OpenAI, Gemini, DeepSeek, etc.) with a single implementation. It automatically prefixes model names (e.g., dashscope/ for Qwen) based on configuration, calls acompletion with tool_choice="auto" when tools are provided, and parses the complex LiteLLM response into a uniform LLMResponse object.

Tool System

All tools reside under agent/tools/ and share a common abstract base Tool defined in base.py. Each tool must implement four members: name – identifier (e.g., "read_file"). description – human‑readable purpose. parameters – JSON‑Schema describing required arguments. execute() – core logic returning a str result.

The base class also provides to_schema() (converts tool metadata to OpenAI function‑calling format) and validate_params() (ensures incoming arguments match the schema).

Tool Registry

ToolRegistry

(in registry.py) maintains a dict[str, Tool] mapping tool names to instances. It offers three key methods: register(tool) – add a tool. get_definitions() – collect to_schema() from all tools for the LLM. execute(name, params) – validate arguments and run the tool, returning an error string prefixed with "Error:" instead of raising exceptions.

Context Construction

The ContextBuilder assembles the system prompt sent to the LLM. It combines:

Identity information ( _get_identity()).

Bootstrap files ( SOUL.md, AGENTS.md, USER.md, TOOLS.md, IDENTITY.md).

Long‑term memory ( MemoryStore) and today’s notes.

Skills – always‑loaded full content plus a summary list for on‑demand loading.

1. _get_identity() → "You are nanobot, an AI assistant", current time, system info, workspace path
2. _load_bootstrap_files() → AGENTS.md, SOUL.md, USER.md, TOOLS.md, IDENTITY.md
3. memory.get_memory_context() → long‑term memory + today’s notes
4. skills → always‑loaded content + other skills summary

After the system prompt, build_messages() appends the conversation history and the current user message to form the final messages list sent to the LLM.

Session Management

session/manager.py

defines Session (a list of {role, content, timestamp} dicts) and SessionManager, which distinguishes sessions by a channel:chat_id key (e.g., feishu:12345 or cli:direct). Sessions are cached in memory and persisted to .jsonl files, where each line stores a single message for efficient appends.

session.add_message("user", msg.content)
session.add_message("assistant", final_content)
sessions.save(session)  # writes to .jsonl

Agent Loop (Core Module)

The AgentLoop class ties everything together. Its __init__ receives the message bus, LLM provider, workspace path, optional API keys, and services such as the cron scheduler. It creates three core components:

self.context = ContextBuilder(workspace)   # context assembly
self.sessions = SessionManager(workspace) # session handling
self.tools = ToolRegistry()              # tool management

The run() coroutine continuously consumes InboundMessage objects from the bus, processes each via _process_message(), and publishes an OutboundMessage back to the bus. The processing flow consists of four stages:

Prepare session : retrieve or create a session.

Assemble context : call context.build_messages() with history, current message, channel, and chat ID.

LLM ↔ Tools loop : repeatedly invoke provider.chat(), check has_tool_calls, execute tools via tools.execute(), and feed results back into the message list. The loop stops after a configurable max_iterations (default 20) to avoid infinite cycles.

Finalize : store the updated session and return the final OutboundMessage.

This implements the classic ReAct pattern (Reasoning → Acting) where the LLM decides whether to call a tool, the tool runs, and the result is fed back for further reasoning.

Channel System (External Chat Integration)

BaseChannel

All channel implementations inherit from BaseChannel (in channels/base.py) and must provide start(), send(msg), and stop(). The base class also offers is_allowed(sender_id) for whitelist checks and _handle_message() to wrap incoming platform messages into InboundMessage and publish them to the bus.

ChannelManager

ChannelManager

(in channels/manager.py) loads configured channel instances, runs them concurrently, and runs a background task that consumes OutboundMessage objects from the bus, routing each to the appropriate channel’s send() method based on msg.channel.

msg = await self.bus.consume_outbound()
channel = self.channels.get(msg.channel)
await channel.send(msg)

Cron Service (Scheduled Tasks)

The cron subsystem defines CronJob objects with an ID, name, schedule (cron expression, fixed interval, or one‑time), payload (message to send, delivery target, and optional channel), and state (last/next run timestamps). The service runs an async timer loop that sleeps until the next scheduled execution, then invokes a callback on_job which forwards the payload as a direct message to the agent via agent.process_direct(). This design reuses the entire Agent Loop for scheduled work.

Heartbeat Service

The heartbeat daemon wakes the agent every 30 minutes, reads workspace/HEARTBEAT.md, and if the file contains content, sends a fixed prompt to the agent. The agent either replies with HEARTBEAT_OK (no action needed) or performs the specified task, providing a simple “alarm clock” for periodic checks such as email reminders.

Subagent Manager (Background Sub‑Tasks)

When a user request would block the main loop for a long time, the SpawnTool creates a separate Subagent process. The sub‑agent runs independently, publishes its result to the message bus, and the main agent forwards the outcome to the user. This keeps the primary conversation responsive while leveraging the existing bus infrastructure.

CLI Entry Points

The cli/commands.py file assembles all components using Typer. Key commands include: nanobot onboard – creates ~/.nanobot/config.json, a workspace directory, and default bootstrap files. nanobot agent -m "..." – single‑message mode; builds the bus, provider, and agent loop, then calls process_direct() to handle the message without routing through channels. nanobot gateway – starts the full system: message bus, provider, agent loop, cron service, heartbeat service, and all configured channels, then runs them concurrently with asyncio.gather().

Additional commands for status, channel login, and CRUD operations on cron jobs.

End‑to‑End Message Flow

A user sends a message on Feishu. The Feishu channel parses the payload, performs whitelist checks, wraps it as an InboundMessage, and publishes it to the bus. The agent loop consumes the message, builds the full context (system prompt, memory, skills, history), calls the LLM, possibly invokes tools (e.g., web_search then write_file), and finally produces an OutboundMessage. This reply is placed on the outbound queue, the channel manager retrieves it, and the Feishu channel sends the reply back to the user.

Conclusion

The Nanobot codebase (~3.5 k lines) delivers a complete AI assistant framework with LLM orchestration, a flexible tool system, multi‑channel support, session persistence, scheduled tasks, heartbeat checks, and background sub‑agents. Its modular design—message bus, context builder, tool registry, and channel manager—offers a clear reference for building similar AI agents.

ArchitecturePythonLLMAI AssistantNanoBot
Su San Talks Tech
Written by

Su San Talks Tech

Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.

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.