Business Workflows
Overview
This document describes the key operational workflows in nanobot. Each workflow includes process steps, sequence diagrams, and code references.
Workflow Index
Workflow 1: Interactive CLI Chat
Overview
The user launches nanobot agent and enters a REPL loop for direct conversation with the LLM.
Sequence Diagram
sequenceDiagram
actor User
participant CLI as CLI (commands.py)
participant Config
participant Provider as LLMProvider
participant AgentLoop
participant Context as ContextBuilder
participant Session as SessionManager
participant Tools as ToolRegistry
User->>CLI: nanobot agent
CLI->>Config: load config.json
CLI->>Provider: initialize provider
CLI->>AgentLoop: create AgentLoop
CLI->>Session: create/load session("cli:default")
loop Interactive REPL
User->>CLI: type message
CLI->>AgentLoop: process message
AgentLoop->>Context: build_system_prompt()
AgentLoop->>Session: get_history()
AgentLoop->>Provider: chat(messages, tools)
Provider-->>AgentLoop: LLMResponse
alt Has tool_calls
AgentLoop->>Tools: execute(tool_name, params)
Tools-->>AgentLoop: result
AgentLoop->>Provider: chat(messages + tool_result)
Provider-->>AgentLoop: LLMResponse
end
AgentLoop->>Session: save_turn()
AgentLoop-->>CLI: response text
CLI-->>User: render Markdown
end
User->>CLI: exit / Ctrl+D
CLI->>Session: persist to disk
Process Steps
Initialization (
nanobot/cli/commands.py:agent)Load config from
~/.nanobot/config.jsonInitialize the LLM provider based on config
Create
AgentLoopwith provider, tools, and session managerSet up
prompt_toolkitfor interactive input with history
Message Input (
nanobot/cli/commands.py:_init_prompt_session)Uses
prompt_toolkit.PromptSessionwithFileHistorySupports multi-line paste, command history, exit commands (
exit,quit,/exit,/quit,:q, Ctrl+D)Optional
-m "message"flag for single-shot mode (no REPL)
Agent Processing (
nanobot/agent/loop.py:AgentLoop)Builds context: identity + bootstrap files + memory + skills
Sends to LLM with tool definitions
Iterates on tool calls until the LLM produces a final text response
Response Rendering (
nanobot/cli/commands.py)Uses
rich.Markdownfor terminal rendering (unless--no-markdown)Shows runtime logs if
--logsflag is set
Code References
CLI entry:
nanobot/cli/commands.py:agent(Typer command)Prompt session:
nanobot/cli/commands.py:_init_prompt_sessionAgent loop:
nanobot/agent/loop.py:AgentLoopContext builder:
nanobot/agent/context.py:ContextBuilder.build_system_prompt
Workflow 2: Gateway Message Processing
Overview
The gateway daemon starts all enabled chat channels and processes messages from any platform through a unified pipeline.
Sequence Diagram
sequenceDiagram
participant Platform as Chat Platform (Telegram/Discord/...)
participant Channel as Channel Adapter
participant Bus as MessageBus
participant AgentLoop
participant Provider as LLMProvider
participant ChannelMgr as ChannelManager
Note over Channel: Channel.start() — connects to platform
Platform->>Channel: incoming message
Channel->>Channel: is_allowed(sender_id)?
alt Denied
Channel-->>Channel: log warning, drop message
else Allowed
Channel->>Bus: publish_inbound(InboundMessage)
end
Bus->>AgentLoop: consume_inbound()
AgentLoop->>AgentLoop: process (context + LLM + tools)
AgentLoop->>Bus: publish_outbound(OutboundMessage)
Bus->>ChannelMgr: consume_outbound()
ChannelMgr->>Channel: send(OutboundMessage)
Channel->>Platform: deliver response
Process Steps
Gateway Startup (
nanobot/cli/commands.py:gateway)Load config
Create
MessageBusCreate
ChannelManager— initializes all enabled channelsCreate
AgentLoop— subscribes to inbound queueStart
HeartbeatServiceandCronServiceLaunch all components as asyncio tasks
Channel Initialization (
nanobot/channels/manager.py:ChannelManager._init_channels)For each enabled channel in config, import the channel class and instantiate it
Lazy imports prevent loading unused SDKs
Message Flow
Channel receives a platform-specific event (webhook, WebSocket, poll)
Channel calls
_handle_message()→ checksis_allowed()→ createsInboundMessage→ publishes to busAgentLoop consumes, processes, and publishes
OutboundMessageChannelManager dispatches to the matching channel’s
send()method
Session Routing
Session key:
{channel}:{chat_id}(e.g.,telegram:12345)Thread-scoped sessions use
session_key_override(e.g., Slack threads)
Code References
Gateway entry:
nanobot/cli/commands.py:gatewayChannel manager:
nanobot/channels/manager.py:ChannelManagerBase channel:
nanobot/channels/base.py:BaseChannelMessage bus:
nanobot/bus/queue.py:MessageBus
Workflow 3: Tool Execution
Overview
When the LLM decides to use a tool, the agent loop executes the tool and feeds the result back into the conversation.
Sequence Diagram
sequenceDiagram
participant AgentLoop
participant LLM as LLM Provider
participant Registry as ToolRegistry
participant Tool as Tool Implementation
AgentLoop->>LLM: chat(messages, tool_schemas)
LLM-->>AgentLoop: LLMResponse(tool_calls=[{name, args}])
loop For each tool_call
AgentLoop->>Registry: get_tool(name)
Registry-->>AgentLoop: Tool instance
AgentLoop->>Tool: validate_params(args)
alt Validation errors
AgentLoop->>AgentLoop: return error as tool result
else Valid
AgentLoop->>Tool: execute(**args)
Tool-->>AgentLoop: result string
end
Note over AgentLoop: Truncate result if > 500 chars
AgentLoop->>AgentLoop: append tool_result to messages
end
AgentLoop->>LLM: chat(messages + tool_results)
LLM-->>AgentLoop: LLMResponse (final text or more tool_calls)
Tool Lifecycle
stateDiagram-v2
[*] --> Registered: Tool added to ToolRegistry
Registered --> SchemaExported: get_schemas() called
SchemaExported --> Selected: LLM picks tool
Selected --> Validated: validate_params()
Validated --> Executing: execute()
Executing --> ResultReturned: return string
ResultReturned --> FedBack: Appended to messages
FedBack --> [*]: LLM processes result
Built-in Tool Details
Tool |
Parameters |
Workspace-Restricted |
Side Effects |
|---|---|---|---|
|
path |
Yes |
None |
|
path, content |
Yes |
Creates/overwrites file |
|
path, old_text, new_text |
Yes |
Patches file |
|
path |
Yes |
None |
|
command |
Yes (PATH configurable) |
Runs shell command |
|
query |
No |
External HTTP |
|
url |
No |
External HTTP |
|
content |
No |
Sends outbound message |
|
task |
No |
Creates subagent |
|
expression, task |
No |
Modifies scheduler |
Code References
Tool base:
nanobot/agent/tools/base.py:ToolTool registry:
nanobot/agent/tools/registry.py:ToolRegistryFile tools:
nanobot/agent/tools/filesystem.pyShell tool:
nanobot/agent/tools/shell.py:ExecToolWeb tools:
nanobot/agent/tools/web.pyMCP bridge:
nanobot/agent/tools/mcp.py
Workflow 4: Heartbeat Periodic Tasks
Overview
The heartbeat service periodically wakes up the agent to check and execute background tasks defined in HEARTBEAT.md.
Sequence Diagram
sequenceDiagram
participant Timer
participant Heartbeat as HeartbeatService
participant LLM as LLM Provider
participant AgentLoop
loop Every 30 minutes
Timer->>Heartbeat: wake up
Heartbeat->>Heartbeat: read HEARTBEAT.md
Heartbeat->>LLM: "Review tasks, call heartbeat(action, tasks)"
alt action = "skip"
LLM-->>Heartbeat: heartbeat(action="skip")
Note over Heartbeat: No tasks to run
else action = "run"
LLM-->>Heartbeat: heartbeat(action="run", tasks="...")
Heartbeat->>AgentLoop: inject tasks as InboundMessage
AgentLoop->>AgentLoop: process tasks normally
AgentLoop->>AgentLoop: deliver results to last active channel
end
end
Process Steps
Timer fires every 30 minutes
Read
HEARTBEAT.mdfrom~/.nanobot/workspace/HEARTBEAT.mdAsk LLM with a special system prompt and the
heartbeattool definitionLLM decides:
skip(nothing to do) orrun(has active tasks)If
run: The task description is injected as an inbound message for the agent loop to process normallyResults delivered to the most recently active chat channel
Code References
Heartbeat service:
nanobot/heartbeat/service.py:HeartbeatServiceHeartbeat tool schema:
nanobot/heartbeat/service.py:_HEARTBEAT_TOOL
Workflow 5: Adding a New Provider
Overview
Adding a new LLM provider to nanobot requires only 2 steps — no if-elif chains.
Steps
Add a
ProviderSpecto thePROVIDERStuple innanobot/providers/registry.py:
ProviderSpec(
name="myprovider",
keywords=("myprovider", "mymodel"),
env_key="MYPROVIDER_API_KEY",
display_name="My Provider",
litellm_prefix="myprovider",
skip_prefixes=("myprovider/",),
)
Add a config field to
ProvidersConfiginnanobot/config/schema.py:
class ProvidersConfig(Base):
...
myprovider: ProviderConfig = ProviderConfig()
The registry-driven design automatically handles:
Environment variable injection for LiteLLM
Model name prefixing (
model→myprovider/model)Config matching (model keywords → provider detection)
Status display in
nanobot status
Code References
Provider registry:
nanobot/providers/registry.pyConfig schema:
nanobot/config/schema.py:ProvidersConfigLiteLLM provider:
nanobot/providers/litellm_provider.py
Workflow 6: Adding a New Channel
Overview
Adding a new chat channel requires implementing the BaseChannel interface and registering it in ChannelManager.
Steps
Create the channel file at
nanobot/channels/myplatform.py:
class MyPlatformChannel(BaseChannel):
name = "myplatform"
async def start(self) -> None:
# Connect to platform, listen for messages
# Call self._handle_message() for each incoming message
pass
async def stop(self) -> None:
# Disconnect, clean up
pass
async def send(self, msg: OutboundMessage) -> None:
# Deliver message to the platform
pass
Add config to
nanobot/config/schema.py:
class MyPlatformConfig(Base):
enabled: bool = False
token: str = ""
allow_from: list[str] = Field(default_factory=list)
class ChannelsConfig(Base):
...
myplatform: MyPlatformConfig = MyPlatformConfig()
Register in ChannelManager at
nanobot/channels/manager.py:_init_channels:
if self.config.channels.myplatform.enabled:
from nanobot.channels.myplatform import MyPlatformChannel
self.channels["myplatform"] = MyPlatformChannel(
self.config.channels.myplatform, self.bus
)
Code References
Base channel:
nanobot/channels/base.py:BaseChannelChannel manager:
nanobot/channels/manager.py:ChannelManager._init_channelsConfig schema:
nanobot/config/schema.py:ChannelsConfig
Error Handling Patterns
Agent Loop Error Recovery
flowchart TD
Start([Receive message]) --> BuildCtx[Build context]
BuildCtx --> CallLLM[Call LLM]
CallLLM --> Check{LLM response?}
Check -->|tool_calls| ExecTool[Execute tool]
ExecTool --> ToolOK{Tool succeeded?}
ToolOK -->|Yes| FeedBack[Feed result to LLM]
ToolOK -->|No| ErrorResult[Return error string as tool result]
ErrorResult --> FeedBack
FeedBack --> CallLLM
Check -->|text response| Send[Send to user]
Check -->|error| LogError[Log error, notify user]
Send --> End([Done])
LogError --> End
Provider Fallback
The registry supports auto-detection of providers based on model name keywords and API key prefixes. If the explicitly configured provider fails, the system logs the error — there is no automatic failover to a different provider (by design, to keep the codebase simple).