Architecture Overview¶
RobotLab is designed around a few core architectural principles that enable flexible, composable AI workflows.
Design Philosophy¶
1. Separation of Concerns¶
Each component has a single, well-defined responsibility:
- Robot: LLM-powered agent (subclass of
RubyLLM::Agent) with personality, tools, and memory - Network: Orchestrates robot execution as a DAG pipeline via SimpleFlow
- Memory: Reactive key-value store for robot and network data
- Tool: Provides external capabilities to robots (inherits from
RubyLLM::Tool) - Task: Wraps a robot for pipeline execution with per-task configuration
2. Composability¶
Components are designed to be mixed and matched:
- Robots can be used standalone or within networks
- Tools can be shared across robots or scoped per-robot via
local_tools: - Networks define DAG pipelines with sequential, parallel, and optional execution
- Memory can be standalone (per-robot) or shared (per-network)
with_*methods returnselffor fluent chaining
3. Provider Agnostic¶
RobotLab abstracts away LLM provider differences through RubyLLM:
- Unified interface across Anthropic, OpenAI, Gemini, DeepSeek, Mistral, and others
- Consistent tool calling interface
- Automatic provider detection from model names
- Easy switching between providers via configuration
System Architecture¶
graph TB
subgraph "Application Layer"
A[Your Application]
end
subgraph "RobotLab Core"
B[Network]
C[Task]
D[Robot < RubyLLM::Agent]
E[Memory]
F[RobotResult]
end
subgraph "Configuration"
G[Config < MywayConfig::Base]
R[RunConfig]
end
subgraph "Cross-Cutting"
HK[Hooks<br/>HookRegistry x3]
end
subgraph "Integration Layer"
H[MCP Client]
I[Tools < RubyLLM::Tool]
J[Templates / prompt_manager]
SK[AgentSkills + Sandbox]
end
subgraph "Execution Layer"
K[SimpleFlow::Pipeline]
L[RubyLLM Chat]
TB[TypedBus + BusPoller]
end
subgraph "Provider Layer"
M[Anthropic]
N[OpenAI]
O[Gemini]
P[MCP Servers]
end
A --> B
A --> D
B --> C
C --> D
B --> K
B --> E
B --> TB
D --> E
D --> L
D --> TB
D --> H
D --> I
D --> J
D --> SK
D --> F
SK --> I
G --> R
R --> B
R --> C
R --> D
G --> D
G --> L
HK -.wraps.-> B
HK -.wraps.-> C
HK -.wraps.-> D
HK -.wraps.-> I
L --> M
L --> N
L --> O
H --> P
The dotted wraps edges are the hook system: Hooks.run brackets every network
run, task, robot run, LLM generation, tool call, compaction, and learn call. It
is the framework's extension seam — the extension gems (robot_lab-audit,
robot_lab-durable, …) attach here rather than subclassing core objects. See the
Hooks API.
Robot subsystems¶
A Robot composes several small collaborators, each documented on the
Support and Skills API pages:
graph LR
D[Robot]
D --> BM[BusMessaging<br/>RobotMessage envelopes]
BM --> BP[BusPoller<br/>per-robot serialization]
D --> BD[Budget::Ledger<br/>token_budget / cost_budget]
D --> DL[DoomLoopDetector<br/>always installed per run]
D --> HC[HistoryCompressor<br/>compress_history / auto_compact]
D --> DF[DelegationFuture<br/>delegate async: true]
D --> HS[HistorySearch<br/>search_history]
D --> ASM[AgentSkillMatching<br/>prepended around run]
ASM --> AS[AgentSkill<br/>SKILL.md bundle]
AS --> CAT[AgentSkillCatalog]
AS --> CAP[Capabilities]
AS --> ST[ScriptTool]
CAP --> SB[Sandbox<br/>Seatbelt or Null]
ST --> SB
HC --> TA[TextAnalysis<br/>TF / TF-IDF]
HS --> TA
CV[Convergence] --> TA
SD[MCP::ServerDiscovery] --> TA
TextAnalysis is the shared floor under every similarity feature — history
compression, history search, convergence detection, and MCP server discovery all
route through it, which is why they share the one optional classifier
dependency.
Core Components¶
| Component | Description | Documentation |
|---|---|---|
| Robot | LLM agent (subclass of RubyLLM::Agent) with template-based prompts, tools, and memory |
Core Concepts |
| Network | Orchestrates multiple robots as a SimpleFlow pipeline | Network Orchestration |
| Memory | Reactive key-value store with pub/sub and blocking reads | Memory Management |
| Task | Wraps a robot for pipeline execution with per-task config | Network Orchestration |
| RobotResult | Captures LLM output, tool calls, and metadata from a run | Message Flow |
| RunConfig | Per-run settings object (LLM fields, mcp/tools, callbacks, infrastructure) that cascades global → network → task → robot |
Core Concepts |
| Config | MywayConfig-based global configuration with env var and file support | Configuration |
| Hook | Handler base class for the seven hook families — the framework's extension seam | Hooks API |
| AgentSkill | A SKILL.md bundle whose instructions and scripts/ become prompt text and tools |
Skills API |
| Sandbox | Opt-in OS-level confinement (macOS Seatbelt) for skill scripts, derived from Capabilities |
Skills API |
| RobotMessage | Immutable envelope for TypedBus inter-robot messaging, serialized per robot by BusPoller |
Support API |
| Budget::Ledger | Thread-safe reserve/reconcile ledger behind token_budget / cost_budget |
Support API |
Configuration¶
Global configuration is a MywayConfig subclass (Config < MywayConfig::Base). It is loaded from multiple sources in priority order:
- Bundled defaults (
lib/robot_lab/config/defaults.yml) - Environment overrides (development, test, production)
- XDG user config (
~/.config/robot_lab/robot_lab.yml— the filename repeats the app name;config.ymlis never read) - Project config (
./config/robot_lab.yml— the only file with an ERB pass) - Environment variables (
ROBOT_LAB_*prefix, double underscore for nesting) - Constructor params
Top-level wrappers behave differently per file. In the XDG user config, a section named for the current environment is honoured — the loader looks for parsed.key?(env) (Anyway::Settings.current_environment, else RAILS_ENV, else RACK_ENV, else "development") and falls back to the root when absent — so both development: and a flat file work there. A defaults: wrapper is not an environment name, so it is ignored. The project config must be flat outside Rails (all wrappers ignored); under Rails, anyway_config sets current_environment to Rails.env and the project file becomes environmental, so a flat file is ignored and keys must sit under development: / test: / production:. See Core Concepts for details.
# Access configuration
RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
RobotLab.config.ruby_llm.request_timeout #=> 120
# Block form (yields the same Config object)
RobotLab.configure do |c|
c.logger = Logger.new($stdout)
end
Data Flow¶
- Input: User calls
robot.run("message")ornetwork.run(message: "...") - Memory: Robot resolves active memory (standalone or network-shared)
- MCP: Robot resolves MCP servers from hierarchical config and connects clients.
rundefaults tomcp: :none, so a plainrunconnects nothing - Tools: Robot resolves and filters tools from hierarchical config.
rundefaults totools: :none, so a plainrunsends the LLM zero tools; passtools: :inheritto send the robot's attached tools - Execution: Robot delegates to
Agent#askwhich calls@chat.askon RubyLLM - Tool Loop: LLM may invoke tools; RubyLLM handles the tool call/result loop
- Result: Robot builds and returns a
RobotResult - Network: If in a network, result flows to dependent tasks via SimpleFlow
Key Patterns¶
Factory Methods¶
Robots and networks are created via factory methods on the RobotLab module:
robot = RobotLab.build(
name: "assistant",
template: :assistant,
context: { tone: "friendly" }
)
network = RobotLab.create_network(name: "pipeline") do
task :analyst, analyst_robot, depends_on: :none
task :writer, writer_robot, depends_on: [:analyst]
end
Fluent Chaining¶
with_* methods on Robot delegate to the underlying @chat and return self:
robot = RobotLab.build(name: "bot")
.with_instructions("Be concise.")
.with_temperature(0.3)
.with_model("gpt-4o")
Hierarchical Configuration¶
Tools and MCP servers use hierarchical resolution: run()/task > robot build-time > network > global config. Values can be :none, :inherit, or an explicit array (which filters the already-attached tools by name — entries must match how the tool was attached, class or instance). run() defaults both to :none, so the runtime level is an explicit "send nothing" unless you override it.
SimpleFlow Pipeline¶
Networks are thin wrappers around SimpleFlow::Pipeline. Each robot is wrapped in a Task that implements the call(result) interface. Tasks define dependencies (:none, [:task_names], or :optional) to control execution order.
Next Steps¶
- Core Concepts - Deep dive into robots and tools
- Robot Execution - How robots process messages
- Network Orchestration - Multi-robot workflows
- Memory Management - Managing memory and reactive features
- Message Flow - How messages move through the system