Class: RobotLab::Robot
- Inherits:
-
RubyLLM::Agent
- Object
- RubyLLM::Agent
- RobotLab::Robot
- Includes:
- Robot::AgentSkillMatching, Robot::Budget, Robot::BusMessaging, Robot::HistorySearch, Robot::Hooking, Robot::MCPManagement, Robot::TemplateRendering, Runnable
- Defined in:
- lib/robot_lab/robot.rb,
lib/robot_lab/robot/budget.rb,
lib/robot_lab/robot/hooking.rb,
lib/robot_lab/robot/bus_messaging.rb,
lib/robot_lab/robot/history_search.rb,
lib/robot_lab/robot/mcp_management.rb,
lib/robot_lab/robot/template_rendering.rb,
lib/robot_lab/robot/agent_skill_matching.rb
Overview
LLM-powered robot built on RubyLLM::Agent
Robot is a subclass of RubyLLM::Agent that adds:
- Template-based prompts via prompt_manager
- Shared memory (standalone or network)
- Tool integration with hierarchical MCP configuration
- SimpleFlow pipeline integration
Memory Behavior
Standalone: Robot uses its own inherent memory (robot.memory).
In a Network: Robot uses the network's shared memory.
Defined Under Namespace
Modules: AgentSkillMatching, Budget, BusMessaging, HistorySearch, Hooking, MCPManagement, TemplateRendering
Constant Summary collapse
- DEFAULT_MAX_TOOLS =
Default ceiling on tools handed to the provider per turn. OpenAI and Anthropic both reject tool arrays longer than 128. Override per robot via RunConfig#max_tools.
128
Instance Attribute Summary collapse
-
#budget_ledger ⇒ Object
readonly
Returns the value of attribute budget_ledger.
-
#bus ⇒ Object
readonly
Returns the value of attribute bus.
-
#config ⇒ Object
readonly
Returns the value of attribute config.
-
#description ⇒ String?
readonly
An optional description of the robot's purpose.
-
#hooks ⇒ Object
readonly
Returns the value of attribute hooks.
-
#input ⇒ IO
Input stream for user interaction (default: $stdin).
-
#learnings ⇒ Object
readonly
Returns the value of attribute learnings.
-
#local_tools ⇒ Array
readonly
The locally defined tools for this robot.
-
#mcp_clients ⇒ Hash<String, MCP::Client>
readonly
Connected MCP clients by server name.
-
#mcp_config ⇒ Symbol, Array
readonly
Build-time MCP configuration (raw, unresolved).
-
#mcp_tools ⇒ Array<Tool>
readonly
Tools discovered from MCP servers.
-
#memory ⇒ Memory
readonly
The robot's inherent memory (used when standalone, not in network).
-
#name ⇒ String
readonly
The unique identifier for the robot.
-
#outbox ⇒ Object
readonly
Returns the value of attribute outbox.
-
#output ⇒ IO
Output stream for user interaction (default: $stdout).
-
#provider ⇒ Object
readonly
Returns the value of attribute provider.
-
#skills ⇒ Object
readonly
Returns the value of attribute skills.
-
#system_prompt ⇒ String?
readonly
Inline system prompt (used alone or appended to template).
-
#template ⇒ Symbol?
readonly
The prompt_manager template for the robot's prompt.
-
#tools_config ⇒ Object
readonly
Returns the value of attribute tools_config.
-
#total_input_tokens ⇒ Object
readonly
Returns the value of attribute total_input_tokens.
-
#total_output_tokens ⇒ Object
readonly
Returns the value of attribute total_output_tokens.
Instance Method Summary collapse
-
#call(result) ⇒ SimpleFlow::Result
SimpleFlow step interface.
-
#chat ⇒ RubyLLM::Chat
Access the underlying RubyLLM::Chat instance.
-
#chat_provider ⇒ String?
Return the provider for this robot's chat.
-
#clear_messages(keep_system: true) ⇒ self
Clear conversation messages, optionally keeping the system prompt.
-
#compress_history(recent_turns: 3, keep_threshold: 0.6, drop_threshold: 0.2, summarizer: nil) ⇒ self
Compress conversation history using TF-IDF relevance scoring.
-
#connect_mcp! ⇒ self
Eagerly connect to configured MCP servers and discover tools.
-
#crew ⇒ Array<Robot>
Runnable protocol: a single robot is a crew of one.
-
#delegate(to:, task:, async: false) ⇒ RobotResult, DelegationFuture
Delegate a task to another robot, synchronously or asynchronously.
-
#disconnect ⇒ self
Disconnect all MCP clients and bus channel.
-
#effective_config ⇒ Hash
Returns the fully-merged configuration for this robot at runtime.
-
#failed_mcp_server_names ⇒ Array<String>
Returns server names that failed to connect.
-
#initialize(name:, template: nil, system_prompt: nil, context: {}, description: nil, local_tools: [], model: nil, provider: nil, mcp_servers: [], mcp: :none, tools: :none, on_tool_call: nil, on_tool_result: nil, on_content: nil, enable_cache: true, bus: nil, skills: nil, temperature: nil, top_p: nil, top_k: nil, max_tokens: nil, presence_penalty: nil, frequency_penalty: nil, stop: nil, max_tool_rounds: nil, token_budget: nil, cost_budget: nil, doom_loop_threshold: nil, mcp_discovery: false, config: nil) ⇒ Robot
constructor
Creates a new Robot instance.
-
#inject_mcp!(clients:, tools:) ⇒ self
Inject pre-connected MCP clients and their tools into this robot.
-
#learn(text) ⇒ self
Add a learning to this robot's accumulation store.
-
#mcp_client(server_name) ⇒ MCP::Client?
Find an MCP client by server name.
-
#messages ⇒ Array<RubyLLM::Message>
Return the conversation messages from the underlying chat.
-
#model ⇒ String?
Returns the model identifier.
-
#replace_messages(messages) ⇒ self
Replace conversation messages with a saved set (for checkpoint restore).
-
#reset_memory ⇒ self
Reset the robot's inherent memory.
-
#reset_token_totals ⇒ self
Reset cumulative token counters to zero.
-
#to_h ⇒ Hash
Converts the robot to a hash representation.
-
#update(template: nil, context: nil, system_prompt: nil, model: nil, temperature: nil, **kwargs) ⇒ self
Reconfigure the robot for a new context.
Methods included from Runnable
#chief, #network?, #robot_count, #single?
Constructor Details
#initialize(name:, template: nil, system_prompt: nil, context: {}, description: nil, local_tools: [], model: nil, provider: nil, mcp_servers: [], mcp: :none, tools: :none, on_tool_call: nil, on_tool_result: nil, on_content: nil, enable_cache: true, bus: nil, skills: nil, temperature: nil, top_p: nil, top_k: nil, max_tokens: nil, presence_penalty: nil, frequency_penalty: nil, stop: nil, max_tool_rounds: nil, token_budget: nil, cost_budget: nil, doom_loop_threshold: nil, mcp_discovery: false, config: nil) ⇒ Robot
Creates a new Robot instance.
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/robot_lab/robot.rb', line 158 def initialize( name:, template: nil, system_prompt: nil, context: {}, description: nil, local_tools: [], model: nil, provider: nil, mcp_servers: [], mcp: :none, tools: :none, on_tool_call: nil, on_tool_result: nil, on_content: nil, enable_cache: true, bus: nil, skills: nil, temperature: nil, top_p: nil, top_k: nil, max_tokens: nil, presence_penalty: nil, frequency_penalty: nil, stop: nil, max_tool_rounds: nil, token_budget: nil, cost_budget: nil, doom_loop_threshold: nil, mcp_discovery: false, config: nil ) validate_tools_filter!(tools) assign_identity_ivars(name: name, template: template, system_prompt: system_prompt, context: context, description: description, local_tools: local_tools, skills: skills, mcp_discovery: mcp_discovery) build_effective_config( model: model, temperature: temperature, top_p: top_p, top_k: top_k, max_tokens: max_tokens, presence_penalty: presence_penalty, frequency_penalty: frequency_penalty, stop: stop, on_tool_call: on_tool_call, on_tool_result: on_tool_result, on_content: on_content, bus: bus, enable_cache: enable_cache, max_tool_rounds: max_tool_rounds, token_budget: token_budget, cost_budget: cost_budget, doom_loop_threshold: doom_loop_threshold, mcp_servers: mcp_servers, mcp: mcp, tools: tools, config: config ) extract_config_ivars initialize_runtime_state initialize_memory lab_config = RobotLab.config resolved_model = @config.model || lab_config.ruby_llm.model chat_kwargs = { model: resolved_model } # RubyLLM auto-sets assume_model_exists for local providers when provider is specified. @provider = provider if @provider chat_kwargs[:provider] = @provider chat_kwargs[:assume_model_exists] = true end super(chat: nil, **chat_kwargs) apply_template apply_system_prompt apply_chat_params register_chat_callbacks end |
Instance Attribute Details
#budget_ledger ⇒ Object (readonly)
Returns the value of attribute budget_ledger.
81 82 83 |
# File 'lib/robot_lab/robot.rb', line 81 def budget_ledger @budget_ledger end |
#bus ⇒ Object (readonly)
Returns the value of attribute bus.
81 82 83 |
# File 'lib/robot_lab/robot.rb', line 81 def bus @bus end |
#config ⇒ Object (readonly)
Returns the value of attribute config.
81 82 83 |
# File 'lib/robot_lab/robot.rb', line 81 def config @config end |
#description ⇒ String? (readonly)
Returns an optional description of the robot's purpose.
79 |
# File 'lib/robot_lab/robot.rb', line 79 attr_accessor :input, :output |
#hooks ⇒ Object (readonly)
Returns the value of attribute hooks.
81 82 83 |
# File 'lib/robot_lab/robot.rb', line 81 def hooks @hooks end |
#input ⇒ IO
Returns input stream for user interaction (default: $stdin).
79 80 81 |
# File 'lib/robot_lab/robot.rb', line 79 def input @input end |
#learnings ⇒ Object (readonly)
Returns the value of attribute learnings.
81 82 83 |
# File 'lib/robot_lab/robot.rb', line 81 def learnings @learnings end |
#local_tools ⇒ Array (readonly)
Returns the locally defined tools for this robot.
79 |
# File 'lib/robot_lab/robot.rb', line 79 attr_accessor :input, :output |
#mcp_clients ⇒ Hash<String, MCP::Client> (readonly)
Returns connected MCP clients by server name.
79 |
# File 'lib/robot_lab/robot.rb', line 79 attr_accessor :input, :output |
#mcp_config ⇒ Symbol, Array (readonly)
Returns build-time MCP configuration (raw, unresolved).
91 92 93 |
# File 'lib/robot_lab/robot.rb', line 91 def mcp_config @mcp_config end |
#mcp_tools ⇒ Array<Tool> (readonly)
Returns tools discovered from MCP servers.
79 |
# File 'lib/robot_lab/robot.rb', line 79 attr_accessor :input, :output |
#memory ⇒ Memory (readonly)
Returns the robot's inherent memory (used when standalone, not in network).
79 |
# File 'lib/robot_lab/robot.rb', line 79 attr_accessor :input, :output |
#name ⇒ String (readonly)
Returns the unique identifier for the robot.
79 |
# File 'lib/robot_lab/robot.rb', line 79 attr_accessor :input, :output |
#outbox ⇒ Object (readonly)
Returns the value of attribute outbox.
81 82 83 |
# File 'lib/robot_lab/robot.rb', line 81 def outbox @outbox end |
#output ⇒ IO
Returns output stream for user interaction (default: $stdout).
79 |
# File 'lib/robot_lab/robot.rb', line 79 attr_accessor :input, :output |
#provider ⇒ Object (readonly)
Returns the value of attribute provider.
81 82 83 |
# File 'lib/robot_lab/robot.rb', line 81 def provider @provider end |
#skills ⇒ Object (readonly)
Returns the value of attribute skills.
81 82 83 |
# File 'lib/robot_lab/robot.rb', line 81 def skills @skills end |
#system_prompt ⇒ String? (readonly)
Returns inline system prompt (used alone or appended to template).
79 |
# File 'lib/robot_lab/robot.rb', line 79 attr_accessor :input, :output |
#template ⇒ Symbol? (readonly)
Returns the prompt_manager template for the robot's prompt.
79 |
# File 'lib/robot_lab/robot.rb', line 79 attr_accessor :input, :output |
#tools_config ⇒ Object (readonly)
Returns the value of attribute tools_config.
91 |
# File 'lib/robot_lab/robot.rb', line 91 attr_reader :mcp_config, :tools_config |
#total_input_tokens ⇒ Object (readonly)
Returns the value of attribute total_input_tokens.
81 82 83 |
# File 'lib/robot_lab/robot.rb', line 81 def total_input_tokens @total_input_tokens end |
#total_output_tokens ⇒ Object (readonly)
Returns the value of attribute total_output_tokens.
81 82 83 |
# File 'lib/robot_lab/robot.rb', line 81 def total_output_tokens @total_output_tokens end |
Instance Method Details
#call(result) ⇒ SimpleFlow::Result
SimpleFlow step interface
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/robot_lab/robot.rb', line 271 def call(result) run_context = extract_run_context(result) # Extract the message from run context = run_context.delete(:message) start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) robot_result = run(, **run_context) robot_result.duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time result .with_context(@name.to_sym, robot_result) .continue(robot_result) rescue Exception => e # rubocop:disable Lint/RescueException # Catch all errors (including SecurityError, Timeout::Error, etc.) # so one failing robot doesn't crash the entire network pipeline. elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time error_result = RobotResult.new( robot_name: @name, output: [TextMessage.new(role: 'assistant', content: "Error: #{e.class}: #{e.}")] ) error_result.duration = elapsed result .with_context(@name.to_sym, error_result) .continue(error_result) end |
#chat ⇒ RubyLLM::Chat
Access the underlying RubyLLM::Chat instance. Useful for checkpoint/restore operations that need direct access to conversation state.
357 358 359 |
# File 'lib/robot_lab/robot.rb', line 357 def chat @chat end |
#chat_provider ⇒ String?
Return the provider for this robot's chat. Useful for displaying model/provider info without reaching into chat internals.
481 482 483 484 485 486 |
# File 'lib/robot_lab/robot.rb', line 481 def chat_provider m = @chat.model m.respond_to?(:provider) ? m.provider : nil rescue StandardError nil end |
#clear_messages(keep_system: true) ⇒ self
Clear conversation messages, optionally keeping the system prompt.
372 373 374 375 376 377 378 379 380 381 |
# File 'lib/robot_lab/robot.rb', line 372 def (keep_system: true) if keep_system system_msg = @chat..find { |m| m.role == :system } @chat. @chat.(system_msg) if system_msg else @chat. end self end |
#compress_history(recent_turns: 3, keep_threshold: 0.6, drop_threshold: 0.2, summarizer: nil) ⇒ self
Compress conversation history using TF-IDF relevance scoring.
Old turns are tiered against the most recent context:
- High relevance (score >= keep_threshold) → kept verbatim
- Medium relevance (drop_threshold..keep_threshold) → summarized or dropped
- Low relevance (score < drop_threshold) → dropped
System messages and tool call/result messages are always preserved.
The most recent recent_turns pairs are also always kept verbatim.
Requires the optional 'classifier' gem (~> 2.3).
Raises DependencyError if not installed.
412 413 414 415 416 417 418 419 420 421 |
# File 'lib/robot_lab/robot.rb', line 412 def compress_history(recent_turns: 3, keep_threshold: 0.6, drop_threshold: 0.2, summarizer: nil) compressed = HistoryCompressor.new( messages: @chat., recent_turns: recent_turns, keep_threshold: keep_threshold, drop_threshold: drop_threshold, summarizer: summarizer ).call (compressed) end |
#connect_mcp! ⇒ self
Eagerly connect to configured MCP servers and discover tools. Normally MCP connections are lazy (established on first run). Call this to connect early, e.g. to display connection status at startup.
312 313 314 315 316 |
# File 'lib/robot_lab/robot.rb', line 312 def connect_mcp! resolved_mcp = resolve_mcp_hierarchy(@mcp_config) ensure_mcp_clients(resolved_mcp) if resolved_mcp.is_a?(Array) && resolved_mcp.any? self end |
#crew ⇒ Array<Robot>
Runnable protocol: a single robot is a crew of one. (network? defaults to false from RobotLab::Runnable.)
97 98 99 |
# File 'lib/robot_lab/robot.rb', line 97 def crew [self] end |
#delegate(to:, task:, async: false) ⇒ RobotResult, DelegationFuture
Delegate a task to another robot, synchronously or asynchronously.
Synchronous (default, async: false): blocks until the delegatee
finishes and returns a RobotResult annotated with delegated_by,
duration, and token counts.
Asynchronous (+async: true+): starts the delegatee in a background
thread and returns a DelegationFuture immediately. Call future.value
to block for the result, or future.resolved? to poll.
451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 |
# File 'lib/robot_lab/robot.rb', line 451 def delegate(to:, task:, async: false, **) if async future = DelegationFuture.new(robot_name: to.name, delegated_by: @name) delegator_name = @name Thread.new do started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) result = to.run(task, **) result.duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at result.delegated_by = delegator_name future.resolve!(result) rescue => e future.reject!(e) end future else started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) result = to.run(task, **) result.duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at result.delegated_by = @name result end end |
#disconnect ⇒ self
Disconnect all MCP clients and bus channel.
330 331 332 333 334 |
# File 'lib/robot_lab/robot.rb', line 330 def disconnect @mcp_clients.each_value(&:disconnect) teardown_bus_channel if @bus self end |
#effective_config ⇒ Hash
Returns the fully-merged configuration for this robot at runtime.
Reflects the result of merging the RunConfig hierarchy (global → network → constructor kwargs → template front matter). Nil fields are omitted.
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/robot_lab/robot.rb', line 111 def effective_config { model: @config.model, temperature: @config.temperature, top_p: @config.top_p, top_k: @config.top_k, max_tokens: @config.max_tokens, presence_penalty: @config.presence_penalty, frequency_penalty: @config.frequency_penalty, stop: @config.stop, tools: @config.tools, mcp: @config.mcp, max_tool_rounds: @config.max_tool_rounds, doom_loop_threshold: @config.doom_loop_threshold, auto_compact: @config.auto_compact, compact_threshold: @config.compact_threshold, token_budget: @config.token_budget, cost_budget: @config.cost_budget }.compact end |
#failed_mcp_server_names ⇒ Array<String>
Returns server names that failed to connect.
321 322 323 324 325 |
# File 'lib/robot_lab/robot.rb', line 321 def failed_mcp_server_names return [] unless @failed_mcp_configs @failed_mcp_configs.keys end |
#inject_mcp!(clients:, tools:) ⇒ self
Inject pre-connected MCP clients and their tools into this robot. Used by host applications (e.g. AIA) that manage MCP connections externally and need to pass them to robots without re-connecting.
345 346 347 348 349 350 |
# File 'lib/robot_lab/robot.rb', line 345 def inject_mcp!(clients:, tools:) @mcp_clients = clients @mcp_tools = tools @mcp_initialized = true self end |
#learn(text) ⇒ self
Add a learning to this robot's accumulation store.
Deduplicates by bidirectional substring matching: a new learning is skipped if it is already contained within an existing learning, or an existing learning is contained within the new one (the new one wins and replaces the weaker entry).
Learnings are persisted to the robot's inherent memory under :learnings.
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 |
# File 'lib/robot_lab/robot.rb', line 507 def learn(text) text = text.to_s.strip return self if text.empty? ctx = LearnHookContext.new(robot: self, text: text, learnings_before: @learnings.dup) registries = [RobotLab.hooks, @hooks] RobotLab::Hooks.run(:learn, ctx, registries: registries) do @learnings.reject! { |existing| text.include?(existing) } unless @learnings.any? { |existing| existing.include?(text) } @learnings << text @memory.set(:learnings, @learnings.dup) ctx.stored = true end RobotLab::Hooks.call(:on_learn, ctx, registries: registries) end self end |
#mcp_client(server_name) ⇒ MCP::Client?
Find an MCP client by server name.
492 493 494 |
# File 'lib/robot_lab/robot.rb', line 492 def mcp_client(server_name) @mcp_clients[server_name] end |
#messages ⇒ Array<RubyLLM::Message>
Return the conversation messages from the underlying chat.
364 365 366 |
# File 'lib/robot_lab/robot.rb', line 364 def @chat. end |
#model ⇒ String?
Returns the model identifier
233 234 235 236 237 238 |
# File 'lib/robot_lab/robot.rb', line 233 def model return nil unless @chat.respond_to?(:model) m = @chat.model m.respond_to?(:id) ? m.id : m.to_s end |
#replace_messages(messages) ⇒ self
Replace conversation messages with a saved set (for checkpoint restore).
387 388 389 390 391 |
# File 'lib/robot_lab/robot.rb', line 387 def () @chat. .each { |m| @chat.(m) } self end |
#reset_memory ⇒ self
Reset the robot's inherent memory
302 303 304 305 |
# File 'lib/robot_lab/robot.rb', line 302 def reset_memory @memory.reset self end |
#reset_token_totals ⇒ self
Reset cumulative token counters to zero.
532 533 534 535 536 |
# File 'lib/robot_lab/robot.rb', line 532 def reset_token_totals @total_input_tokens = 0 @total_output_tokens = 0 self end |
#to_h ⇒ Hash
Converts the robot to a hash representation
541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 |
# File 'lib/robot_lab/robot.rb', line 541 def to_h { name: name, description: description, template: template, skills: @skills, system_prompt: system_prompt, local_tools: local_tools.map { |t| t.respond_to?(:name) ? t.name : t.to_s }, mcp_tools: mcp_tools.map(&:name), mcp_config: @mcp_config, tools_config: @tools_config, mcp_servers: @mcp_clients.keys, model: model, config: (@config.empty? ? nil : @config.to_json_hash), bus: @bus ? true : nil }.compact end |
#update(template: nil, context: nil, system_prompt: nil, model: nil, temperature: nil, **kwargs) ⇒ self
Reconfigure the robot for a new context
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 |
# File 'lib/robot_lab/robot.rb', line 248 def update(template: nil, context: nil, system_prompt: nil, model: nil, temperature: nil, **kwargs) if template @template = template ctx = context || @build_context apply_template_to_chat(ctx) end @chat.with_instructions(system_prompt) if system_prompt @chat.with_model(model) if model @chat.with_temperature(temperature) if temperature kwargs.each do |key, value| method = :"with_#{key}" @chat.public_send(method, value) if value && @chat.respond_to?(method) end self end |