Engineering Conventions

Code Style

Language & Version

  • Python ≥ 3.11 — uses X | Y union syntax, match statements, StrEnum, etc.

  • Line length: 100 characters (pyproject.toml: [tool.ruff] line-length = 100)

  • Target version: py311

Linting & Formatting

nanobot uses ruff for linting with the following rule sets:

Code

Rules

E

pycodestyle errors

F

Pyflakes

I

isort (import sorting)

N

pep8-naming

W

pycodestyle warnings

Ignored: E501 (line length) — the 100-char limit is a guideline, not enforced.

Run linter:

ruff check nanobot/

Import Organization

Imports follow this order (enforced by ruff’s I rules):

# 1. Future annotations (always first when used)
from __future__ import annotations

# 2. Standard library
import asyncio
import json
from pathlib import Path
from typing import Any, TYPE_CHECKING

# 3. Third-party libraries
from loguru import logger
from pydantic import BaseModel

# 4. Local imports
from nanobot.bus.events import InboundMessage
from nanobot.agent.tools.base import Tool

TYPE_CHECKING guard: Heavy imports used only for type hints go inside if TYPE_CHECKING: blocks to avoid circular imports and speed up module loading.

Type Annotations

  • All public function signatures must have type annotations

  • Use str | None syntax (not Optional[str])

  • Use list[str] (not List[str])

  • Use dict[str, Any] (not Dict[str, Any])

Naming Conventions

Element

Convention

Example

Modules

snake_case.py

litellm_provider.py

Classes

PascalCase

AgentLoop, MessageBus

Functions / Methods

snake_case

build_system_prompt()

Private methods

_snake_case

_handle_message()

Constants

UPPER_SNAKE_CASE

BUILTIN_SKILLS_DIR

Config fields (Python)

snake_case

api_key, allow_from

Config fields (JSON)

camelCase

apiKey, allowFrom

Dataclass fields

snake_case

sender_id, chat_id

Test functions

test_descriptive_name

test_validate_params_missing_required

Configuration Naming

The Pydantic Base model uses alias_generator=to_camel so that:

  • Python code uses snake_case: config.providers.openrouter.api_key

  • JSON config accepts both: "apiKey" or "api_key"

This dual-naming is intentional — it maintains Python idiom in code while being compatible with Claude Desktop / Cursor config formats that use camelCase.

Architecture Patterns

Abstract Base Classes

Core abstractions use Python ABCs:

  • LLMProvider — abstract chat() and get_default_model()

  • BaseChannel — abstract start(), stop(), send()

  • Tool — abstract name, description, parameters, execute()

Registry Pattern

The provider system uses a declarative registry (ProviderSpec entries in a tuple) instead of if-elif chains. Adding a new provider means adding data, not modifying control flow.

Message Bus (Pub/Sub)

Channels and the agent loop are decoupled through asyncio.Queue. This means:

  • Channels don’t know about the agent

  • The agent doesn’t know about channels

  • All coordination happens through InboundMessage / OutboundMessage dataclasses

Lazy Imports

Channel SDKs are imported lazily inside ChannelManager._init_channels() — only when the channel is enabled. This avoids loading heavy SDKs (telegram, discord, slack, etc.) for channels the user hasn’t configured.

Append-Only Sessions

Session messages are never modified or deleted. Consolidation advances the last_consolidated pointer but leaves old messages intact. This design:

  • Preserves LLM prompt cache efficiency (prefixes don’t change)

  • Simplifies persistence (append to JSONL)

  • Avoids data loss from failed consolidation

Error Handling

Tool Execution

The ToolRegistry.execute() method wraps all tool calls in try/except:

  • Validation errors → return error string with [Analyze the error above and try a different approach.] hint

  • Execution errors → return error string with the same hint

  • The LLM receives error strings as tool results and can self-correct

Provider Errors

Provider exceptions (API errors, rate limits, timeouts) are:

  • Logged via loguru

  • Propagated to the agent loop

  • Communicated to the user as error messages

Channel Errors

Channel connection failures are:

  • Logged as warnings

  • The channel is marked as unavailable

  • Other channels continue to operate independently

Logging

  • Library: loguru (structured logging with colors)

  • Default output: stderr

  • Log format: timestamp, level, module, message

  • CLI flag: --logs shows logs alongside chat output in interactive mode

  • Best practice: Use logger.info() for key events, logger.warning() for recoverable issues, logger.exception() for unexpected failures

Testing

Framework

  • pytest + pytest-asyncio (mode: auto)

  • Tests in tests/ directory

  • Test files: test_*.py

Patterns

  • Unit tests: Test individual functions/methods with no external dependencies

  • Mocking: Use DummyProvider, SampleTool etc. — lightweight test doubles defined in test files

  • Async tests: Decorated with @pytest.mark.asyncio, use tmp_path fixture for temp files

  • Fixtures: tmp_path (pytest built-in) for isolated filesystem tests

Running Tests

# Run all tests
pytest -s tests/

# Run a specific test file
pytest -s tests/test_tool_validation.py

# Run with verbose output
pytest -v tests/

# Run tests matching a pattern
pytest -k "test_heartbeat" tests/

Git & Contribution

Branch Strategy

  • PRs welcome against the main branch

  • The codebase is intentionally small and readable

Commit Style

  • Descriptive commit messages

  • No enforced commit message format

Code Size Budget

The project maintains a ~4,000 line code size target for core agent code. The script core_agent_lines.sh verifies this.

Dependency Management

Adding Dependencies

  • Add to pyproject.toml under [project.dependencies]

  • Pin major version ranges: "typer>=0.20.0,<1.0.0"

  • Optional dependencies go in [project.optional-dependencies]

Current Dependency Groups

Group

Purpose

Example Packages

Core

Always installed

typer, litellm, pydantic, httpx, loguru, rich

Channels

Chat platform SDKs

python-telegram-bot, slack-sdk, qq-botpy

Matrix

Optional Matrix support

matrix-nio, mistune, nh3

Dev

Development tools

pytest, pytest-asyncio, ruff

Skills Convention

Structure

Each skill lives in a directory with a SKILL.md file:

skills/
└── my-skill/
    └── SKILL.md

Frontmatter

Skills use YAML frontmatter for metadata:

---
name: weather
description: Query weather information for any location
metadata: '{"nanobot": {"requires": {"bins": ["curl"]}, "always": false}}'
---

Priority

Workspace skills (~/.nanobot/workspace/skills/) override built-in skills (nanobot/skills/) when names collide.