Architecture

joshbot · Go 1.24 · ~14,500 LOC · 520+ tests

System Architecture

joshbot is built around a goroutine-based message bus that decouples chat channels from a ReAct agent loop. The agent is backed by a multi-provider LLM layer via OpenRouter-compatible APIs. Everything — memory, skills, tools — is file-based, making the system portable and auditable without a database.

Core Data I/O LLM Infrastructure
CLI Channel
bufio.Reader stdin
Telegram Channel
telebot long-polling
chan InboundMessage
Message Bus
goroutine channel multiplexer
routed to agent goroutine
Agent (ReAct Loop)
max 20 iterations: think → act → observe
Tools Registry
20 built-in tools
Memory System
MEMORY.md + HISTORY.md
Skills System
SKILL.md auto-discovery
LLM Providers
OpenRouter, Anthropic, etc.
chan OutboundMessage
Message Bus
response routing back to channel
CLI Output
stdout
Telegram Output
bot.send()

Message Processing Pipeline

Every message — from CLI or Telegram — follows the same path through the system.

1
Channel Receives Message
CLI reads stdin via bufio.Reader. Telegram polls updates via telebot long-polling. Both produce an InboundMessage.
2
Bus Routes to Agent
The message bus fans InboundMessage into the agent goroutine via a channel. The bus handles multiplexing across channels.
3
Agent Assembles Context
Identity files (IDENTITY.md, SOUL.md, USER.md, AGENTS.md) + MEMORY.md + skills summaries → system prompt. Session history loaded from JSONL files.
4
ReAct Loop Begins
LLM responds with text and/or tool calls. Agent executes tools via the Registry, feeds results back as observations. Repeats up to 20 iterations.
5
Memory & Skills Update
After each response, key facts are extracted into MEMORY.md. Repeated tool patterns trigger auto-skill creation via detection/extraction pipeline.
6
Response Sent Back
Final response → OutboundMessage on bus → routed to correct channel → displayed to user.

Code Map

The project follows Go conventions with cmd/ for the entry point and internal/ for all implementation packages.

joshbot/ ├── cmd/joshbot/ ← CLI entry point, service wiring (~3,340 LOC) │ ├── main.go entry point, flags, gateway │ └── main_test.go ├── internal/ ← all implementation │ ├── agent/ ReAct loop, context assembly, prompt caching │ ├── memory/ structured fact store (fact.go, search.go) │ ├── skills/ discovery, detection, extraction, validation │ ├── tools/ 20 built-in tools + registry │ ├── channels/ CLI + Telegram channel implementations │ ├── bus/ goroutine message bus │ ├── providers/ LLM provider layer (OpenRouter, Anthropic, etc.) │ ├── session/ JSONL conversation persistence │ ├── subagent/ restricted subagent runner │ ├── cron/ task scheduler │ ├── heartbeat/ proactive wake-up checker │ ├── context/ budget manager, compression │ ├── config/ JSON config, env overrides │ ├── learning/ summary extraction │ ├── log/ structured logging (charmbracelet/log) │ ├── copilot/ GitHub Copilot auth │ ├── configure/ config wizard │ ├── service/ systemd/launchd install │ └── integration/ integration tests ├── docs/ documentation ├── skills/ workspace skills (SKILL.md files) ├── site/ ← website (this page) ├── README.md ├── AGENTS.md ├── CLAUDE.md ├── CHANGELOG.md └── VERSION

Key Architectural Patterns

These design decisions shape how every component works.

Message Bus

Channels decoupled from agent via chan InboundMessage and chan OutboundMessage. Adding a new channel requires zero agent changes.

ReAct Loop

LLM → tools → reflect → repeat (max 20 iterations). Each iteration appends tool results as observations for the next LLM call.

Plain-File Storage

No databases. MEMORY.md, HISTORY.md, SKILL.md files. Portable, auditable, grep-able, syncs via Dropbox/git.

Progressive Skill Loading

Skills summarized in context; full content loaded on demand. Keeps token overhead minimal until a skill is actually invoked.

Prompt Caching

Static system prompt segments cached with mtime-based invalidation. Reduces file I/O on every message — critical for memory/skill files read each turn.

Model-Centric Config

Provider auto-detected from model prefix (claude → Anthropic, gpt → OpenAI). Fallback chains for resilience. ExtraBody support for provider-specific fields.

Context Compression

Conversation history summarized when approaching token limits. Budget manager tracks and enforces per-iteration costs.

Subagent Delegation

Parallel fan-out or sequential chain execution. Each subagent gets a focused one-turn LLM call with its own system prompt. Results merged back into the parent agent context.

Key Components

Agent (internal/agent/)

The core ReAct loop. Each message triggers up to 20 think-act-observe iterations. System prompt is assembled from identity files + memory + skills summaries (with mtime-based caching). The agent serializes the tool schema from the Registry into the LLM's function calling format.

Tools (internal/tools/)

20 tools implement the Tool interface. Each declares Name, Description, Parameters, and Execute. The Registry handles discovery and execution. Tools include: filesystem, shell (with safety deny-list), web fetch (SSRF-protected), memory search, skill management, parallel subagent, chain execution, message sending, and more.

Memory System (internal/memory/)

Two files: MEMORY.md (always in context — key facts, preferences, decisions) and HISTORY.md (grep-searchable event log for recall). Facts are extracted after each conversation turn by the learning module.

Skills System (internal/skills/)

Markdown files with YAML frontmatter in workspace/skills/{name}/SKILL.md. Auto-discovered on startup. Progressive loading: summary only in system prompt, full content loaded when the skill is invoked. The detection module observes tool usage patterns and auto-creates skills.

Providers (internal/providers/)

Multi-provider LLM support via a unified Provider interface. Registered providers: OpenRouter, OpenAI, NVIDIA, Groq, Ollama, Anthropic, Poolside, Azure, Custom, LiteLLM. Provider auto-selection by model prefix. ExtraBody support for provider-specific fields (e.g., poolside's chat_template_kwargs).

Message Bus (internal/bus/)

Goroutine-safe channel multiplexer. Inbound messages from any channel are routed to the agent goroutine. Outbound messages from the agent are routed back to the originating channel. This decoupling means channels and agent can be developed independently.

Model-Centric Config

Providers are auto-detected from model names. The config supports both legacy and model-centric formats.

{ "models_config": { "models": [ { "name": "smart", "model": "anthropic/claude-sonnet-4", "api_key": "..." }, { "name": "fast", "model": "groq/llama-3.3-70b", "api_key": "..." }, { "name": "code", "model": "poolside/laguna-m.1", "api_key": "...", "extra_body": { "chat_template_kwargs": { "enable_thinking": false } } } ], "agent": { "model": "smart", "fallback": ["fast"] } }, "providers": { "openrouter": { "api_key": "...", "enabled": true }, "nvidia": { "api_key": "...", "enabled": true } }, "channels": { "telegram": { "enabled": false, "token": "", "allow_from": [] } } }

Environment variable overrides: JOSHBOT_PROVIDERS__OPENROUTER__API_KEY or JOSHBOT_MODELS_CONFIG__AGENT__MODEL. Config is validated JSON, stored at ~/.joshbot/config.json.