Class: RobotLab::Robot

Inherits:
RubyLLM::Agent
  • Object
show all
Includes:
BusMessaging, HistorySearch, MCPManagement, TemplateRendering
Defined in:
lib/robot_lab/robot.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

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.

Examples:

Simple robot with template

robot = Robot.new(name: "helper", template: :helper)
result = robot.run("Hello!")

Robot with inline system prompt

robot = Robot.new(name: "bot", system_prompt: "You are helpful.")
result = robot.run("What is 2+2?")

Bare robot configured via chaining

robot = Robot.new(name: "bot")
robot.with_instructions("Be concise.").run("Hello")

Robot with tools

robot = Robot.new(
  name: "support",
  template: :support,
  local_tools: [OrderLookup, RefundProcessor]
)

Defined Under Namespace

Modules: BusMessaging, HistorySearch, MCPManagement, TemplateRendering

Constant Summary

Constants included from HistorySearch

HistorySearch::MIN_SCORE_LENGTH

Constants included from TemplateRendering

TemplateRendering::FRONT_MATTER_CONFIG_KEYS, TemplateRendering::FRONT_MATTER_EXTRA_KEYS

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from HistorySearch

#search_history

Methods included from BusMessaging

#assign_bus_poller, #on_message, #send_message, #send_reply, #spawn, #with_bus

Methods included from TemplateRendering

#with_template

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, mcp_discovery: false, config: nil) ⇒ Robot

Creates a new Robot instance.

Parameters:

  • name (String)

    the unique identifier for the robot

  • template (Symbol, nil) (defaults to: nil)

    the prompt_manager template

  • system_prompt (String, nil) (defaults to: nil)

    inline system prompt

  • context (Hash, Proc) (defaults to: {})

    variables to pass to the template

  • description (String, nil) (defaults to: nil)

    optional description

  • local_tools (Array) (defaults to: [])

    tools defined locally

  • model (String, nil) (defaults to: nil)

    the LLM model to use

  • mcp_servers (Array) (defaults to: [])

    legacy parameter for MCP server configurations

  • mcp (Symbol, Array) (defaults to: :none)

    hierarchical MCP config

  • tools (Symbol, Array) (defaults to: :none)

    hierarchical tools config

  • on_tool_call (Proc, nil) (defaults to: nil)

    callback invoked when a tool is called

  • on_tool_result (Proc, nil) (defaults to: nil)

    callback invoked when a tool returns a result

  • on_content (Proc, nil) (defaults to: nil)

    callback invoked with each streaming content chunk

  • enable_cache (Boolean) (defaults to: true)

    whether to enable semantic caching

  • bus (TypedBus::MessageBus, nil) (defaults to: nil)

    optional message bus for inter-robot communication

  • temperature (Float, nil) (defaults to: nil)

    controls randomness

  • top_p (Float, nil) (defaults to: nil)

    nucleus sampling threshold

  • top_k (Integer, nil) (defaults to: nil)

    top-k sampling

  • max_tokens (Integer, nil) (defaults to: nil)

    maximum tokens in response

  • presence_penalty (Float, nil) (defaults to: nil)

    penalize based on presence

  • frequency_penalty (Float, nil) (defaults to: nil)

    penalize based on frequency

  • stop (String, Array, nil) (defaults to: nil)

    stop sequences

  • skills (Symbol, Array<Symbol>, nil) (defaults to: nil)

    skill templates to prepend

  • config (RunConfig, nil) (defaults to: nil)

    shared configuration (merged with explicit kwargs)



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
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 106

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,
  mcp_discovery: false,
  config: nil
)
  @name = name.to_s
  @name_from_constructor = (name.to_s != "robot")
  @template = template
  @system_prompt = system_prompt
  @build_context = context
  @description = description
  @local_tools = Array(local_tools)
  @skills = skills ? Array(skills).map(&:to_sym) : nil
  @expanded_skills = nil
  @mcp_discovery = mcp_discovery

  # Build RunConfig from explicit kwargs, merged on top of passed-in config.
  # Explicit constructor kwargs always override the shared config.
  explicit_fields = {
    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
  }.compact

  # Only include mcp/tools if explicitly set (not the default :none sentinel)
  resolved_mcp = mcp_servers.any? ? mcp_servers : mcp
  explicit_fields[:mcp] = resolved_mcp unless ToolConfig.none_value?(resolved_mcp)
  explicit_fields[:tools] = tools unless ToolConfig.none_value?(tools)

  explicit_config = RunConfig.new(**explicit_fields)
  @config = config ? config.merge(explicit_config) : explicit_config

  # Extract values from effective config for backward compatibility
  @on_tool_call = @config.on_tool_call
  @on_tool_result = @config.on_tool_result
  @on_content = @config.on_content

  # Store raw config values for hierarchical resolution
  @mcp_config = @config.mcp || :none
  @tools_config = @config.tools || :none

  # MCP state
  @mcp_clients = {}
  @mcp_tools = []
  @mcp_initialized = false

  # Bus state (optional inter-robot communication)
  @bus               = @config.bus
  @message_counter   = 0
  @outbox            = {}
  @message_handler   = nil
  @bus_poller        = nil
  @private_bus_poller = nil
  @bus_poller_group  = :default

  # Token tracking
  @total_input_tokens = 0
  @total_output_tokens = 0

  # Learning accumulation
  @learnings = []

  # Inherent memory (used when standalone, not in a network)
  cache_enabled = @config.key?(:enable_cache) ? @config.enable_cache : true
  @memory = Memory.new(enable_cache: cache_enabled)

  # Restore persisted learnings from inherent memory if present
  persisted = @memory.get(:learnings)
  @learnings = Array(persisted) if persisted

  # Ensure config is loaded (triggers PM setup, RubyLLM config, etc.)
  config = RobotLab.config

  # Build chat kwargs for Agent's super
  resolved_model = @config.model || config.ruby_llm.model
  chat_kwargs = { model: resolved_model }

  # Pass provider through for local providers (Ollama, GPUStack, etc.)
  # 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

  # Create the persistent chat via Agent's initialize
  super(chat: nil, **chat_kwargs)

  # Dynamically delegate all with_* methods from the Chat class
  define_chat_delegators

  # Gather all skill IDs from constructor + main template front matter
  all_skill_ids = Array(@skills)
  if @template
    parsed_main = PM.parse(@template)
    fm_skills = (parsed_main.)
    all_skill_ids = all_skill_ids + fm_skills
  end

  if all_skill_ids.any?
    # Skills path: expand skills, merge config, concatenate bodies
    apply_skills_and_template_to_chat(all_skill_ids, context)
  elsif @template
    # Standard path: single template, no skills
    apply_template_to_chat(context)
  end

  @chat.with_instructions(@system_prompt) if @system_prompt

  # Constructor params override template front matter (use config values)
  @chat.with_temperature(@config.temperature) if @config.temperature

  # These parameters don't have dedicated with_* methods on Chat;
  # pass them through with_params.
  extra_params = {
    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
  }.compact
  @chat.with_params(**extra_params) if extra_params.any?

  # Apply callbacks
  @chat.on_tool_call(&@on_tool_call) if @on_tool_call
  @chat.on_tool_result(&@on_tool_result) if @on_tool_result

  # Set up bus channel if a bus was provided
  setup_bus_channel if @bus
end

Instance Attribute Details

#busObject (readonly)

Returns the value of attribute bus.



69
70
71
# File 'lib/robot_lab/robot.rb', line 69

def bus
  @bus
end

#configObject (readonly)

Returns the value of attribute config.



69
70
71
# File 'lib/robot_lab/robot.rb', line 69

def config
  @config
end

#descriptionString? (readonly)

Returns an optional description of the robot’s purpose.

Returns:

  • (String, nil)

    an optional description of the robot’s purpose



67
# File 'lib/robot_lab/robot.rb', line 67

attr_accessor :input, :output

#inputIO

Returns input stream for user interaction (default: $stdin).

Returns:

  • (IO)

    input stream for user interaction (default: $stdin)



67
68
69
# File 'lib/robot_lab/robot.rb', line 67

def input
  @input
end

#learningsObject (readonly)

Returns the value of attribute learnings.



69
70
71
# File 'lib/robot_lab/robot.rb', line 69

def learnings
  @learnings
end

#local_toolsArray (readonly)

Returns the locally defined tools for this robot.

Returns:

  • (Array)

    the locally defined tools for this robot



67
# File 'lib/robot_lab/robot.rb', line 67

attr_accessor :input, :output

#mcp_clientsHash<String, MCP::Client> (readonly)

Returns connected MCP clients by server name.

Returns:

  • (Hash<String, MCP::Client>)

    connected MCP clients by server name



67
# File 'lib/robot_lab/robot.rb', line 67

attr_accessor :input, :output

#mcp_configSymbol, Array (readonly)

Returns build-time MCP configuration (raw, unresolved).

Returns:

  • (Symbol, Array)

    build-time MCP configuration (raw, unresolved)



78
79
80
# File 'lib/robot_lab/robot.rb', line 78

def mcp_config
  @mcp_config
end

#mcp_toolsArray<Tool> (readonly)

Returns tools discovered from MCP servers.

Returns:

  • (Array<Tool>)

    tools discovered from MCP servers



67
# File 'lib/robot_lab/robot.rb', line 67

attr_accessor :input, :output

#memoryMemory (readonly)

Returns the robot’s inherent memory (used when standalone, not in network).

Returns:

  • (Memory)

    the robot’s inherent memory (used when standalone, not in network)



67
# File 'lib/robot_lab/robot.rb', line 67

attr_accessor :input, :output

#nameString (readonly)

Returns the unique identifier for the robot.

Returns:

  • (String)

    the unique identifier for the robot



67
# File 'lib/robot_lab/robot.rb', line 67

attr_accessor :input, :output

#outboxObject (readonly)

Returns the value of attribute outbox.



69
70
71
# File 'lib/robot_lab/robot.rb', line 69

def outbox
  @outbox
end

#outputIO

Returns output stream for user interaction (default: $stdout).

Returns:

  • (IO)

    output stream for user interaction (default: $stdout)



67
# File 'lib/robot_lab/robot.rb', line 67

attr_accessor :input, :output

#providerObject (readonly)

Returns the value of attribute provider.



69
70
71
# File 'lib/robot_lab/robot.rb', line 69

def provider
  @provider
end

#skillsObject (readonly)

Returns the value of attribute skills.



69
70
71
# File 'lib/robot_lab/robot.rb', line 69

def skills
  @skills
end

#system_promptString? (readonly)

Returns inline system prompt (used alone or appended to template).

Returns:

  • (String, nil)

    inline system prompt (used alone or appended to template)



67
# File 'lib/robot_lab/robot.rb', line 67

attr_accessor :input, :output

#templateSymbol? (readonly)

Returns the prompt_manager template for the robot’s prompt.

Returns:

  • (Symbol, nil)

    the prompt_manager template for the robot’s prompt



67
# File 'lib/robot_lab/robot.rb', line 67

attr_accessor :input, :output

#tools_configObject (readonly)

Returns the value of attribute tools_config.



78
# File 'lib/robot_lab/robot.rb', line 78

attr_reader :mcp_config, :tools_config

#total_input_tokensObject (readonly)

Returns the value of attribute total_input_tokens.



69
70
71
# File 'lib/robot_lab/robot.rb', line 69

def total_input_tokens
  @total_input_tokens
end

#total_output_tokensObject (readonly)

Returns the value of attribute total_output_tokens.



69
70
71
# File 'lib/robot_lab/robot.rb', line 69

def total_output_tokens
  @total_output_tokens
end

Instance Method Details

#call(result) ⇒ SimpleFlow::Result

SimpleFlow step interface

Parameters:

  • result (SimpleFlow::Result)

    incoming result from previous step

Returns:

  • (SimpleFlow::Result)

    result with robot output



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/robot_lab/robot.rb', line 404

def call(result)
  run_context = extract_run_context(result)

  # Extract the message from run context
  message = run_context.delete(:message)

  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  robot_result = run(message, **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.message}")]
  )
  error_result.duration = elapsed

  result
    .with_context(@name.to_sym, error_result)
    .continue(error_result)
end

#chatRubyLLM::Chat

Access the underlying RubyLLM::Chat instance. Useful for checkpoint/restore operations that need direct access to conversation state.

Returns:

  • (RubyLLM::Chat)


493
494
495
# File 'lib/robot_lab/robot.rb', line 493

def chat
  @chat
end

#chat_providerString?

Return the provider for this robot’s chat. Useful for displaying model/provider info without reaching into chat internals.

Returns:

  • (String, nil)


616
617
618
619
620
621
# File 'lib/robot_lab/robot.rb', line 616

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.

Parameters:

  • keep_system (Boolean) (defaults to: true)

    whether to preserve the system message

Returns:

  • (self)


508
509
510
511
512
513
514
515
516
517
# File 'lib/robot_lab/robot.rb', line 508

def clear_messages(keep_system: true)
  if keep_system
    system_msg = @chat.messages.find { |m| m.role == :system }
    @chat.instance_variable_set(:@messages, [])
    @chat.add_message(system_msg) if system_msg
  else
    @chat.instance_variable_set(:@messages, [])
  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.

Parameters:

  • recent_turns (Integer) (defaults to: 3)

    turn pairs to protect at the end (default 3)

  • keep_threshold (Float) (defaults to: 0.6)

    cosine score >= this → keep verbatim (default 0.6)

  • drop_threshold (Float) (defaults to: 0.2)

    cosine score < this → drop (default 0.2)

  • summarizer (#call, nil) (defaults to: nil)

    callable(text) -> String for medium-tier; nil drops medium-tier instead of summarizing

Returns:

  • (self)


547
548
549
550
551
552
553
554
555
556
# File 'lib/robot_lab/robot.rb', line 547

def compress_history(recent_turns: 3, keep_threshold: 0.6, drop_threshold: 0.2, summarizer: nil)
  compressed = HistoryCompressor.new(
    messages:       @chat.messages,
    recent_turns:   recent_turns,
    keep_threshold: keep_threshold,
    drop_threshold: drop_threshold,
    summarizer:     summarizer
  ).call
  replace_messages(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.

Returns:

  • (self)


447
448
449
450
451
# File 'lib/robot_lab/robot.rb', line 447

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

#delegate(to:, task:, async: false, **kwargs) ⇒ 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.

Examples:

Synchronous

result = manager.delegate(to: analyst, task: "What are the risks?")
puts result.reply          # analyst's answer
puts result.delegated_by   # => "manager"
puts result.duration       # => 1.43 (seconds)

Async fan-out

f1 = manager.delegate(to: summarizer, task: "summarize ...", async: true)
f2 = manager.delegate(to: analyst,    task: "analyze ...",   async: true)
summary  = f1.value          # blocks if not yet done
analysis = f2.value(timeout: 30)

Parameters:

  • to (Robot)

    the robot to delegate to

  • task (String)

    the message to send

  • async (Boolean) (defaults to: false)

    when true, returns a DelegationFuture immediately

  • kwargs (Hash)

    additional keyword args forwarded to Robot#run

Returns:



586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
# File 'lib/robot_lab/robot.rb', line 586

def delegate(to:, task:, async: false, **kwargs)
  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, **kwargs)
      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, **kwargs)
    result.duration     = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
    result.delegated_by = @name
    result
  end
end

#disconnectself

Disconnect all MCP clients and bus channel.

Returns:

  • (self)


465
466
467
468
469
# File 'lib/robot_lab/robot.rb', line 465

def disconnect
  @mcp_clients.each_value(&:disconnect)
  teardown_bus_channel if @bus
  self
end

#failed_mcp_server_namesArray<String>

Returns server names that failed to connect.

Returns:

  • (Array<String>)


456
457
458
459
460
# File 'lib/robot_lab/robot.rb', line 456

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.

Parameters:

  • clients (Hash<String, MCP::Client>)

    connected MCP clients by server name

  • tools (Array<Tool>)

    tools discovered from the MCP servers

Returns:

  • (self)


481
482
483
484
485
486
# File 'lib/robot_lab/robot.rb', line 481

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.

Parameters:

  • text (String)

    the insight to record

Returns:

  • (self)


643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
# File 'lib/robot_lab/robot.rb', line 643

def learn(text)
  text = text.to_s.strip
  return self if text.empty?

  # Remove any existing learning that is a substring of the new one
  @learnings.reject! { |existing| text.include?(existing) }

  # Skip if any existing learning already covers the new one
  unless @learnings.any? { |existing| existing.include?(text) }
    @learnings << text
    @memory.set(:learnings, @learnings.dup)
  end

  self
end

#mcp_client(server_name) ⇒ MCP::Client?

Find an MCP client by server name.

Parameters:

  • server_name (String)

    the MCP server name

Returns:



627
628
629
# File 'lib/robot_lab/robot.rb', line 627

def mcp_client(server_name)
  @mcp_clients[server_name]
end

#messagesArray<RubyLLM::Message>

Return the conversation messages from the underlying chat.

Returns:

  • (Array<RubyLLM::Message>)


500
501
502
# File 'lib/robot_lab/robot.rb', line 500

def messages
  @chat.messages
end

#modelString?

Returns the model identifier

Returns:

  • (String, nil)

    the LLM model ID string



271
272
273
274
275
276
# File 'lib/robot_lab/robot.rb', line 271

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).

Parameters:

  • messages (Array<RubyLLM::Message>)

    the messages to restore

Returns:

  • (self)


523
524
525
526
# File 'lib/robot_lab/robot.rb', line 523

def replace_messages(messages)
  @chat.instance_variable_set(:@messages, messages)
  self
end

#reset_memoryself

Reset the robot’s inherent memory

Returns:

  • (self)


436
437
438
439
# File 'lib/robot_lab/robot.rb', line 436

def reset_memory
  @memory.reset
  self
end

#reset_token_totalsself

Reset cumulative token counters to zero.

Returns:

  • (self)


663
664
665
666
667
# File 'lib/robot_lab/robot.rb', line 663

def reset_token_totals
  @total_input_tokens = 0
  @total_output_tokens = 0
  self
end

#run(message = nil, network: nil, network_memory: nil, network_config: nil, memory: nil, mcp: :none, tools: :none, **kwargs) {|chunk| ... } ⇒ RobotResult

Send a message and get a response, with Robot’s extended capabilities

Parameters:

  • message (String) (defaults to: nil)

    the user message

  • network (NetworkRun, nil) (defaults to: nil)

    network context (legacy)

  • network_memory (Memory, nil) (defaults to: nil)

    shared network memory

  • memory (Memory, Hash, nil) (defaults to: nil)

    runtime memory to merge

  • mcp (Symbol, Array, nil) (defaults to: :none)

    runtime MCP override

  • tools (Symbol, Array, nil) (defaults to: :none)

    runtime tools override

Yields:

  • (chunk)

    optional streaming block called with each content chunk

Returns:



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/robot_lab/robot.rb', line 302

def run(message = nil, network: nil, network_memory: nil, network_config: nil,
        memory: nil, mcp: :none, tools: :none, **kwargs, &block)
  # Determine which memory to use
  run_memory = resolve_active_memory(network: network, network_memory: network_memory)

  # Merge runtime memory if provided
  case memory
  when Memory
    run_memory = memory
  when Hash
    run_memory.merge!(memory)
  end

  # Set current_writer so memory notifications know who wrote the value
  previous_writer = run_memory.current_writer
  run_memory.current_writer = @name

  begin
    # Resolve hierarchical MCP and tools configuration
    resolved_mcp = resolve_mcp_hierarchy(mcp, network: network, network_config: network_config)
    resolved_tools = resolve_tools_hierarchy(tools, network: network, network_config: network_config)

    # Filter MCP servers by semantic relevance when discovery is enabled.
    # Only applies on the first run (before @mcp_initialized) so connections
    # are not torn down mid-conversation.
    if @mcp_discovery && !@mcp_initialized && resolved_mcp.is_a?(Array)
      resolved_mcp = MCP::ServerDiscovery.select(message.to_s, from: resolved_mcp)
    end

    # Initialize or update MCP clients based on resolved config
    ensure_mcp_clients(resolved_mcp)

    # Apply filtered tools to the persistent chat
    filtered = filtered_tools(resolved_tools)
    @chat.with_tools(*filtered) if filtered.any?

    # Re-render template with run-time context merged into build-time context.
    # Template parameters (e.g. customer: null) may require values that are
    # only available at run time — the robot gathers them before rendering.
    run_context = kwargs.except(:with)
    rerender_template(run_context) if @template && run_context.any?

    # Prepend accumulated learnings to the user message
    effective_message = inject_learnings(message)

    # Install circuit breaker for this run if max_tool_rounds is configured
    install_circuit_breaker if @config.max_tool_rounds

    # Delegate to Agent's ask (which calls @chat.ask)
    ask_kwargs = kwargs.slice(:with)
    streaming = effective_streaming_block(block)
    response = ask(effective_message, **ask_kwargs, &streaming)

    result = build_result(response, run_memory)

    # Enforce token budget if configured
    budget = @config.token_budget
    if budget && @total_input_tokens + @total_output_tokens > budget
      raise InferenceError,
            "Token budget exceeded: #{@total_input_tokens + @total_output_tokens} tokens used, budget is #{budget}"
    end

    result
  ensure
    restore_tool_call_callback if @config.max_tool_rounds
    run_memory.current_writer = previous_writer
  end
end

#to_hHash

Converts the robot to a hash representation

Returns:

  • (Hash)


673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
# File 'lib/robot_lab/robot.rb', line 673

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

Parameters:

  • template (Symbol, nil) (defaults to: nil)

    new template to apply

  • context (Hash, nil) (defaults to: nil)

    new context for the template

  • system_prompt (String, nil) (defaults to: nil)

    new system prompt

  • model (String, nil) (defaults to: nil)

    new model

  • temperature (Float, nil) (defaults to: nil)

    new temperature

Returns:

  • (self)


380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/robot_lab/robot.rb', line 380

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