Source tree and reading order
Corresponding source
| Reading layer | Directory or file |
|---|---|
| Python product code | nanobot/ |
| Agent core | nanobot/agent/loop.py, runner.py, context.py, tools/ |
| Runtime surfaces | nanobot/cli/, api/, sdk/, channels/, gateway/, webui/ |
| Frontend and Bridge | webui/, bridge/ |
| Executable behavior specifications | tests/ |
| Official usage documentation | docs/ |
| Packaging entry | pyproject.toml, hatch_build.py |
Learning objectives
After this lesson, you should be able to:
- explain the responsibilities of the main directories;
- map user-visible behavior to the responsible module;
- read by call chain instead of mechanically traversing files;
- distinguish the core, integration, and presentation layers.
Prerequisites
- Read Five entry points and their runtime boundaries.
- Know the basic meanings of AgentLoop, Provider, Tool, and Session.
Project root
| Path | Responsibility | Beginner priority |
|---|---|---|
nanobot/ | Python runtime | High |
tests/ | Python behavior and regression tests | High |
docs/ | Current repository usage and development documentation | High |
webui/ | React/TypeScript WebUI | Later |
bridge/ | Integrations requiring a separate bridge service | Optional |
pyproject.toml | package, dependencies, CLI entry point, plugin entry points | Medium |
.agent/ | design, security, and common pitfalls | High |
Map of nanobot/
| Directory | Primary responsibility | Read first |
|---|---|---|
agent/ | Turn, Runner, Context, Memory, Tools, Subagent | loop.py, runner.py, context.py |
bus/ | inbound/outbound queues and runtime events | events.py, queue.py, runtime_events.py |
session/ | Session JSONL, keys, goals, continuations, WebUI turn coordination | manager.py, keys.py |
providers/ | LLM Provider contracts, registration, creation, fallback, adapters | base.py, registry.py, factory.py |
channels/ | external chat platforms and the WebSocket adapter | base.py, registry.py, manager.py |
config/ | Pydantic schema, loading, paths | schema.py, loader.py |
cli/ | Typer CLI and the main runtime assembly entry point | commands.py |
api/ | OpenAI-compatible HTTP API | server.py |
sdk/ | SDK types, streams, and runtime helpers | enter through nanobot/nanobot.py |
cron/ | workspace-scoped scheduling, bound Turns, delivery | service.py, bound_runner.py |
command/ | slash-command routing and built-in commands | router.py, builtin.py |
pairing/ | pairing approval for unauthorized direct-message senders | store.py |
security/ | workspace and network boundaries | workspace_policy.py, network.py |
webui/ | Python-side WebUI API, tokens, threads, and media coordination | choose by problem |
apps/ | App protocol and CLI app service | do not confuse with Skill or Tool |
audio/ | transcription Provider registration and dispatch | Optional |
skills/ | built-in Skill files | start with README.md |
templates/ | system prompt, bootstrap, Dream, and Heartbeat templates | read while studying context |
Core and edge layers
The project design keeps the core small and places capabilities at the edges:
flowchart TD CORE["Core path<br/>AgentLoop + AgentRunner"] CTX["Context / Session / Provider contracts"] --> CORE CORE --> TOOLS["Tools"] CORE --> EVENTS["General runtime events"] CH["Channels"] --> CORE MCP["MCP"] --> TOOLS SK["Skills"] --> CTX EVENTS --> UI["WebUI / WebSocket coordination"] CRON["Cron / Heartbeat"] --> CORE
Design implications:
- prefer Tool, Skill, MCP, or Channel for a new capability;
- put behavior in AgentLoop/Runner only when it is general Turn behavior that cannot be expressed at the edge;
- keep UI wire details out of AgentLoop;
- allow limited duplication in Providers and Channels instead of forcing a difficult universal base class.
Source evidence:
.agent/design.md
Recommended reading order
Pass 1: Read only the skeleton
nanobot/bus/events.pynanobot/bus/queue.pynanobot/agent/loop.py::TurnStateAgentLoop.process_directAgentLoop._process_messageAgentRunner.runAgentRunner._run_core
Goal: draw only the primary call chain. Do not enter every helper.
Pass 2: Add context and state
nanobot/agent/context.pynanobot/session/manager.pynanobot/session/keys.pynanobot/agent/memory.pynanobot/bus/runtime_events.py
Goal: answer “What did the model see, what was saved to disk, and what did the UI receive?”
Pass 3: Add Provider and Tool
nanobot/providers/base.pynanobot/providers/factory.pynanobot/providers/registry.pynanobot/agent/tools/base.pynanobot/agent/tools/registry.pynanobot/agent/tools/loader.py
Goal: answer “How is the model selected, how are tools discovered, how are arguments validated, and how are results reinserted?”
Pass 4: Choose one runtime surface
Select a path:
- CLI:
nanobot/cli/commands.py - API:
nanobot/api/server.py - SDK:
nanobot/nanobot.py - Channel:
nanobot/channels/base.py→manager.py→ one concrete Channel - WebUI:
nanobot/channels/websocket.py→session/webui_turns.py→ Python WebUI API → React
Pass 5: Advanced mechanisms
agent/autocompact.pyagent/memory.pysession/turn_continuation.pysession/goal_state.pyagent/subagent.pycron/security/
Test reading order
Do not start by running the full test suite. Choose the file closest to the subject:
| Subject | Test starting point |
|---|---|
| Loop/Runner main path | tests/agent/test_loop_runner_integration.py |
| Runner basic branches | tests/agent/test_runner_core.py |
| Tool execution | tests/agent/test_runner_tool_execution.py |
| Errors and safety | tests/agent/test_runner_errors.py, test_runner_safety.py |
| Context | tests/agent/test_context_builder.py |
| Session | tests/agent/test_session_manager_history.py, test_session_atomic.py |
| Runtime events | tests/bus/test_runtime_events.py |
| Provider | tests/providers/ |
| Channel | tests/channels/test_base_channel.py, test_channel_plugins.py |
| Subagent | tests/agent/test_subagent_lifecycle.py |
Locate a directory from a symptom
| Symptom | Inspect first |
|---|---|
| wrong model or key | config/, providers/ |
| Tool schema or argument error | agent/tools/, agent/runner.py |
| broken history ordering in one conversation | agent/loop.py, session/ |
| Channel does not receive or send | channels/, pairing/ |
| UI state does not refresh | bus/runtime_events.py, session/webui_turns.py, channels/websocket.py |
| Cron does not run | cron/, Gateway assembly, workspace cron store |
| filesystem access escapes its boundary | agent/tools/filesystem.py, security/ |
| HTTP tool reaches a private network | security/network.py, Web/MCP callers |
| prompt behavior changes | agent/context.py, templates/, Skills, Session history |
Common reading mistakes
Mistake 1: Inferring responsibility from a filename
gateway/runtime.py sounds like the entire Gateway runtime, but the main service assembly path is cli/commands.py::_run_gateway.
Mistake 2: Treating tests as a substitute for implementation
Tests prove protected behavior, but they do not replace the actual call chain. Verify the entry point and boundary that each test covers.
Mistake 3: Assuming every entry point is identical because code is shared
API, SDK, and CLI can all call process_direct(), but their session keys, stream adapters, locks, and error formats differ.
Mistake 4: Forgetting the shared contract after entering one Provider
Understand LLMProvider and LLMResponse before reading the OpenAI-compatible adapter used by DeepSeek.
Self-check
- If WebUI emits an incorrect
_turn_end, which modules should you inspect first? - If a new capability only teaches the model how to complete a task, why should you consider a Skill first?
- If the API returns 400 while CLI works, why should you begin with
api/server.py? - Why should the bar for changing AgentLoop/Runner be higher than for adding a Tool?
Next step
Read the minimal troubleshooting flow, then continue to One Turn.