robot_lab 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.architecture/AGENTS.md +32 -0
- data/.architecture/config.yml +8 -0
- data/.architecture/members.yml +60 -0
- data/.architecture/reviews/feature-free-will.md +490 -0
- data/.architecture/reviews/overall-codebase.md +427 -0
- data/.claude/settings.local.json +57 -0
- data/.codex/config.toml +2 -0
- data/.irbrc +2 -2
- data/.rubocop.yml +172 -0
- data/CHANGELOG.md +72 -0
- data/CLAUDE.md +139 -0
- data/README.md +91 -95
- data/Rakefile +109 -3
- data/agent2agent_review.md +192 -0
- data/agentf_improvements.md +253 -0
- data/agents.md +14 -0
- data/docs/examples/index.md +37 -2
- data/docs/getting-started/configuration.md +20 -7
- data/docs/guides/index.md +16 -16
- data/docs/guides/knowledge.md +7 -1
- data/docs/guides/observability.md +132 -0
- data/docs/index.md +30 -3
- data/docs/superpowers/plans/2026-05-06-agentskills.md +1303 -0
- data/docs/superpowers/specs/2026-05-06-agentskills-design.md +247 -0
- data/examples/.envrc +1 -0
- data/examples/01_simple_robot.rb +5 -9
- data/examples/02_tools.rb +5 -9
- data/examples/03_network.rb +8 -9
- data/examples/04_mcp.rb +21 -29
- data/examples/05_streaming.rb +12 -18
- data/examples/06_prompt_templates.rb +11 -19
- data/examples/07_network_memory.rb +16 -31
- data/examples/08_llm_config.rb +10 -22
- data/examples/09_chaining.rb +16 -27
- data/examples/10_memory.rb +12 -28
- data/examples/11_network_introspection.rb +15 -29
- data/examples/12_message_bus.rb +5 -12
- data/examples/13_spawn.rb +5 -10
- data/examples/14_rusty_circuit/.envrc +1 -0
- data/examples/14_rusty_circuit/comic.rb +2 -0
- data/examples/14_rusty_circuit/heckler.rb +1 -1
- data/examples/14_rusty_circuit/open_mic.rb +1 -3
- data/examples/14_rusty_circuit/scout.rb +2 -0
- data/examples/15_memory_network_and_bus/.envrc +1 -0
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +6 -3
- data/examples/15_memory_network_and_bus/linux_writer.rb +1 -1
- data/examples/15_memory_network_and_bus/output/combined_article.md +6 -6
- data/examples/15_memory_network_and_bus/output/final_article.md +6 -8
- data/examples/15_memory_network_and_bus/output/linux_draft.md +4 -2
- data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
- data/examples/15_memory_network_and_bus/output/memory.json +6 -6
- data/examples/15_memory_network_and_bus/output/revision_1.md +10 -11
- data/examples/15_memory_network_and_bus/output/revision_2.md +6 -8
- data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
- data/examples/16_writers_room/.envrc +1 -0
- data/examples/16_writers_room/writers_room.rb +2 -4
- data/examples/17_skills.rb +8 -17
- data/examples/18_rails/Gemfile +1 -0
- data/examples/19_token_tracking.rb +9 -15
- data/examples/20_circuit_breaker.rb +10 -19
- data/examples/21_learning_loop.rb +11 -20
- data/examples/22_context_compression.rb +6 -13
- data/examples/23_convergence.rb +6 -17
- data/examples/24_structured_delegation.rb +11 -15
- data/examples/25_history_search.rb +5 -12
- data/examples/26_document_store.rb +6 -13
- data/examples/27_incident_response/incident_response.rb +4 -5
- data/examples/28_mcp_discovery.rb +8 -11
- data/examples/29_ractor_tools.rb +4 -9
- data/examples/30_ractor_network.rb +10 -19
- data/examples/31_launch_assessment.rb +10 -23
- data/examples/32_newsletter_reader.rb +188 -0
- data/examples/33_stock_generator.rb +80 -0
- data/examples/33_stock_predictor.rb +306 -0
- data/examples/34_agentskills.rb +72 -0
- data/examples/README.md +1 -1
- data/examples/common.rb +76 -0
- data/examples/ruboruby.md +423 -0
- data/examples/temp.md +51 -0
- data/lib/robot_lab/agent_skill.rb +63 -0
- data/lib/robot_lab/agent_skill_catalog.rb +74 -0
- data/lib/robot_lab/ask_user.rb +2 -2
- data/lib/robot_lab/bus_poller.rb +12 -5
- data/lib/robot_lab/config.rb +1 -12
- data/lib/robot_lab/delegation_future.rb +1 -1
- data/lib/robot_lab/doom_loop_detector.rb +98 -0
- data/lib/robot_lab/history_compressor.rb +4 -10
- data/lib/robot_lab/mcp/client.rb +1 -2
- data/lib/robot_lab/mcp/connection_poller.rb +3 -3
- data/lib/robot_lab/mcp/server.rb +1 -1
- data/lib/robot_lab/mcp/server_discovery.rb +0 -2
- data/lib/robot_lab/memory.rb +32 -27
- data/lib/robot_lab/memory_change.rb +2 -2
- data/lib/robot_lab/message.rb +4 -4
- data/lib/robot_lab/network.rb +11 -6
- data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
- data/lib/robot_lab/robot/bus_messaging.rb +9 -27
- data/lib/robot_lab/robot/history_search.rb +4 -1
- data/lib/robot_lab/robot/mcp_management.rb +5 -11
- data/lib/robot_lab/robot/template_rendering.rb +60 -40
- data/lib/robot_lab/robot.rb +323 -206
- data/lib/robot_lab/robot_result.rb +6 -5
- data/lib/robot_lab/run_config.rb +5 -11
- data/lib/robot_lab/script_tool.rb +76 -0
- data/lib/robot_lab/state_proxy.rb +7 -5
- data/lib/robot_lab/tool.rb +3 -3
- data/lib/robot_lab/tool_config.rb +1 -1
- data/lib/robot_lab/tool_manifest.rb +5 -7
- data/lib/robot_lab/user_message.rb +2 -2
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab/waiter.rb +1 -1
- data/lib/robot_lab.rb +41 -52
- data/logfile +8 -0
- data/mkdocs.yml +2 -3
- data/robot_concurrency.md +38 -0
- data/simple_acp_review.md +298 -0
- data/site/404.html +2300 -0
- data/site/api/core/index.html +2706 -0
- data/site/api/core/memory/index.html +3793 -0
- data/site/api/core/network/index.html +3500 -0
- data/site/api/core/robot/index.html +4566 -0
- data/site/api/core/state/index.html +3390 -0
- data/site/api/core/tool/index.html +3843 -0
- data/site/api/index.html +2635 -0
- data/site/api/mcp/client/index.html +3435 -0
- data/site/api/mcp/index.html +2783 -0
- data/site/api/mcp/server/index.html +3252 -0
- data/site/api/mcp/transports/index.html +3352 -0
- data/site/api/messages/index.html +2641 -0
- data/site/api/messages/text-message/index.html +3087 -0
- data/site/api/messages/tool-call-message/index.html +3159 -0
- data/site/api/messages/tool-result-message/index.html +3252 -0
- data/site/api/messages/user-message/index.html +3212 -0
- data/site/api/streaming/context/index.html +3282 -0
- data/site/api/streaming/events/index.html +3347 -0
- data/site/api/streaming/index.html +2738 -0
- data/site/architecture/core-concepts/index.html +3757 -0
- data/site/architecture/index.html +2797 -0
- data/site/architecture/message-flow/index.html +3238 -0
- data/site/architecture/network-orchestration/index.html +3433 -0
- data/site/architecture/robot-execution/index.html +3140 -0
- data/site/architecture/state-management/index.html +3498 -0
- data/site/assets/css/custom.css +56 -0
- data/site/assets/images/favicon.png +0 -0
- data/site/assets/images/robot_lab.jpg +0 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js +16 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js.map +7 -0
- data/site/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
- data/site/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
- data/site/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
- data/site/assets/javascripts/lunr/tinyseg.js +206 -0
- data/site/assets/javascripts/lunr/wordcut.js +6708 -0
- data/site/assets/javascripts/workers/search.2c215733.min.js +42 -0
- data/site/assets/javascripts/workers/search.2c215733.min.js.map +7 -0
- data/site/assets/stylesheets/main.484c7ddc.min.css +1 -0
- data/site/assets/stylesheets/main.484c7ddc.min.css.map +1 -0
- data/site/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
- data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +1 -0
- data/site/concepts/index.html +3455 -0
- data/site/examples/basic-chat/index.html +2880 -0
- data/site/examples/index.html +2907 -0
- data/site/examples/mcp-server/index.html +3018 -0
- data/site/examples/multi-robot-network/index.html +3131 -0
- data/site/examples/rails-application/index.html +3329 -0
- data/site/examples/tool-usage/index.html +3085 -0
- data/site/getting-started/configuration/index.html +3745 -0
- data/site/getting-started/index.html +2572 -0
- data/site/getting-started/installation/index.html +2981 -0
- data/site/getting-started/quick-start/index.html +2942 -0
- data/site/guides/building-robots/index.html +4290 -0
- data/site/guides/creating-networks/index.html +3858 -0
- data/site/guides/index.html +2586 -0
- data/site/guides/mcp-integration/index.html +3581 -0
- data/site/guides/memory/index.html +3586 -0
- data/site/guides/rails-integration/index.html +4019 -0
- data/site/guides/streaming/index.html +3157 -0
- data/site/guides/using-tools/index.html +3802 -0
- data/site/index.html +2671 -0
- data/site/search/search_index.json +1 -0
- data/site/sitemap.xml +183 -0
- data/site/sitemap.xml.gz +0 -0
- data/site/tags.json +1 -0
- data/temp.md +6 -0
- data/tool_manifest_plan.md +155 -0
- metadata +154 -92
- data/docs/examples/rails-application.md +0 -419
- data/docs/guides/ractor-parallelism.md +0 -364
- data/docs/guides/rails-integration.md +0 -681
- data/docs/superpowers/plans/2026-04-14-ractor-integration.md +0 -1538
- data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +0 -258
- data/lib/generators/robot_lab/install_generator.rb +0 -90
- data/lib/generators/robot_lab/job_generator.rb +0 -40
- data/lib/generators/robot_lab/robot_generator.rb +0 -55
- data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -42
- data/lib/generators/robot_lab/templates/job.rb.tt +0 -21
- data/lib/generators/robot_lab/templates/migration.rb.tt +0 -32
- data/lib/generators/robot_lab/templates/result_model.rb.tt +0 -52
- data/lib/generators/robot_lab/templates/robot.rb.tt +0 -31
- data/lib/generators/robot_lab/templates/robot_job.rb.tt +0 -18
- data/lib/generators/robot_lab/templates/robot_test.rb.tt +0 -34
- data/lib/generators/robot_lab/templates/routing_robot.rb.tt +0 -59
- data/lib/generators/robot_lab/templates/thread_model.rb.tt +0 -40
- data/lib/robot_lab/document_store.rb +0 -155
- data/lib/robot_lab/ractor_boundary.rb +0 -42
- data/lib/robot_lab/ractor_job.rb +0 -37
- data/lib/robot_lab/ractor_memory_proxy.rb +0 -85
- data/lib/robot_lab/ractor_network_scheduler.rb +0 -154
- data/lib/robot_lab/ractor_worker_pool.rb +0 -117
- data/lib/robot_lab/rails_integration/engine.rb +0 -29
- data/lib/robot_lab/rails_integration/job.rb +0 -158
- data/lib/robot_lab/rails_integration/railtie.rb +0 -51
- data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +0 -72
data/lib/robot_lab/robot.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative 'robot/template_rendering'
|
|
|
4
4
|
require_relative 'robot/mcp_management'
|
|
5
5
|
require_relative 'robot/bus_messaging'
|
|
6
6
|
require_relative 'robot/history_search'
|
|
7
|
+
require_relative 'robot/agent_skill_matching'
|
|
7
8
|
|
|
8
9
|
module RobotLab
|
|
9
10
|
# LLM-powered robot built on RubyLLM::Agent
|
|
@@ -43,6 +44,7 @@ module RobotLab
|
|
|
43
44
|
include Robot::MCPManagement
|
|
44
45
|
include Robot::BusMessaging
|
|
45
46
|
include Robot::HistorySearch
|
|
47
|
+
prepend Robot::AgentSkillMatching
|
|
46
48
|
|
|
47
49
|
# @!attribute [r] name
|
|
48
50
|
# @return [String] the unique identifier for the robot
|
|
@@ -69,7 +71,8 @@ module RobotLab
|
|
|
69
71
|
attr_reader :name, :description, :template, :system_prompt,
|
|
70
72
|
:local_tools, :mcp_clients, :mcp_tools, :memory,
|
|
71
73
|
:bus, :outbox, :config, :skills, :provider,
|
|
72
|
-
:total_input_tokens, :total_output_tokens, :learnings
|
|
74
|
+
:total_input_tokens, :total_output_tokens, :learnings,
|
|
75
|
+
:durable_store, :learn_domain
|
|
73
76
|
|
|
74
77
|
# @!attribute [r] mcp_config
|
|
75
78
|
# @return [Symbol, Array] build-time MCP configuration (raw, unresolved)
|
|
@@ -77,6 +80,36 @@ module RobotLab
|
|
|
77
80
|
# @return [Symbol, Array] build-time tools configuration (raw, unresolved)
|
|
78
81
|
attr_reader :mcp_config, :tools_config
|
|
79
82
|
|
|
83
|
+
# Returns the fully-merged configuration for this robot at runtime.
|
|
84
|
+
#
|
|
85
|
+
# Reflects the result of merging the RunConfig hierarchy (global → network →
|
|
86
|
+
# constructor kwargs → template front matter). Nil fields are omitted.
|
|
87
|
+
#
|
|
88
|
+
# @return [Hash] merged config keyed by field name
|
|
89
|
+
#
|
|
90
|
+
# @example
|
|
91
|
+
# robot.effective_config
|
|
92
|
+
# #=> { model: "claude-sonnet-4-6", temperature: 0.7, max_tokens: 4096 }
|
|
93
|
+
def effective_config
|
|
94
|
+
{
|
|
95
|
+
model: @config.model,
|
|
96
|
+
temperature: @config.temperature,
|
|
97
|
+
top_p: @config.top_p,
|
|
98
|
+
top_k: @config.top_k,
|
|
99
|
+
max_tokens: @config.max_tokens,
|
|
100
|
+
presence_penalty: @config.presence_penalty,
|
|
101
|
+
frequency_penalty: @config.frequency_penalty,
|
|
102
|
+
stop: @config.stop,
|
|
103
|
+
tools: @config.tools,
|
|
104
|
+
mcp: @config.mcp,
|
|
105
|
+
max_tool_rounds: @config.max_tool_rounds,
|
|
106
|
+
doom_loop_threshold: @config.doom_loop_threshold,
|
|
107
|
+
auto_compact: @config.auto_compact,
|
|
108
|
+
compact_threshold: @config.compact_threshold,
|
|
109
|
+
token_budget: @config.token_budget
|
|
110
|
+
}.compact
|
|
111
|
+
end
|
|
112
|
+
|
|
80
113
|
# Creates a new Robot instance.
|
|
81
114
|
#
|
|
82
115
|
# @param name [String] the unique identifier for the robot
|
|
@@ -130,141 +163,52 @@ module RobotLab
|
|
|
130
163
|
stop: nil,
|
|
131
164
|
max_tool_rounds: nil,
|
|
132
165
|
token_budget: nil,
|
|
166
|
+
doom_loop_threshold: nil,
|
|
133
167
|
mcp_discovery: false,
|
|
134
|
-
config: nil
|
|
168
|
+
config: nil,
|
|
169
|
+
learn: false,
|
|
170
|
+
learn_domain: nil,
|
|
171
|
+
store_path: nil
|
|
135
172
|
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
@system_prompt = system_prompt
|
|
140
|
-
@build_context = context
|
|
141
|
-
@description = description
|
|
142
|
-
@local_tools = Array(local_tools)
|
|
143
|
-
@skills = skills ? Array(skills).map(&:to_sym) : nil
|
|
144
|
-
@expanded_skills = nil
|
|
145
|
-
@mcp_discovery = mcp_discovery
|
|
173
|
+
assign_identity_ivars(name: name, template: template, system_prompt: system_prompt,
|
|
174
|
+
context: context, description: description, local_tools: local_tools,
|
|
175
|
+
skills: skills, mcp_discovery: mcp_discovery)
|
|
146
176
|
|
|
147
|
-
|
|
148
|
-
# Explicit constructor kwargs always override the shared config.
|
|
149
|
-
explicit_fields = {
|
|
177
|
+
build_effective_config(
|
|
150
178
|
model: model, temperature: temperature, top_p: top_p, top_k: top_k,
|
|
151
179
|
max_tokens: max_tokens, presence_penalty: presence_penalty,
|
|
152
180
|
frequency_penalty: frequency_penalty, stop: stop,
|
|
153
181
|
on_tool_call: on_tool_call, on_tool_result: on_tool_result,
|
|
154
182
|
on_content: on_content, bus: bus, enable_cache: enable_cache,
|
|
155
|
-
max_tool_rounds: max_tool_rounds, token_budget: token_budget
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
resolved_mcp = mcp_servers.any? ? mcp_servers : mcp
|
|
160
|
-
explicit_fields[:mcp] = resolved_mcp unless ToolConfig.none_value?(resolved_mcp)
|
|
161
|
-
explicit_fields[:tools] = tools unless ToolConfig.none_value?(tools)
|
|
162
|
-
|
|
163
|
-
explicit_config = RunConfig.new(**explicit_fields)
|
|
164
|
-
@config = config ? config.merge(explicit_config) : explicit_config
|
|
165
|
-
|
|
166
|
-
# Extract values from effective config for backward compatibility
|
|
167
|
-
@on_tool_call = @config.on_tool_call
|
|
168
|
-
@on_tool_result = @config.on_tool_result
|
|
169
|
-
@on_content = @config.on_content
|
|
170
|
-
|
|
171
|
-
# Store raw config values for hierarchical resolution
|
|
172
|
-
@mcp_config = @config.mcp || :none
|
|
173
|
-
@tools_config = @config.tools || :none
|
|
174
|
-
|
|
175
|
-
# MCP state
|
|
176
|
-
@mcp_clients = {}
|
|
177
|
-
@mcp_tools = []
|
|
178
|
-
@mcp_initialized = false
|
|
179
|
-
|
|
180
|
-
# Bus state (optional inter-robot communication)
|
|
181
|
-
@bus = @config.bus
|
|
182
|
-
@message_counter = 0
|
|
183
|
-
@outbox = {}
|
|
184
|
-
@message_handler = nil
|
|
185
|
-
@bus_poller = nil
|
|
186
|
-
@private_bus_poller = nil
|
|
187
|
-
@bus_poller_group = :default
|
|
188
|
-
|
|
189
|
-
# Token tracking
|
|
190
|
-
@total_input_tokens = 0
|
|
191
|
-
@total_output_tokens = 0
|
|
192
|
-
|
|
193
|
-
# Learning accumulation
|
|
194
|
-
@learnings = []
|
|
195
|
-
|
|
196
|
-
# Inherent memory (used when standalone, not in a network)
|
|
197
|
-
cache_enabled = @config.key?(:enable_cache) ? @config.enable_cache : true
|
|
198
|
-
@memory = Memory.new(enable_cache: cache_enabled)
|
|
199
|
-
|
|
200
|
-
# Restore persisted learnings from inherent memory if present
|
|
201
|
-
persisted = @memory.get(:learnings)
|
|
202
|
-
@learnings = Array(persisted) if persisted
|
|
183
|
+
max_tool_rounds: max_tool_rounds, token_budget: token_budget,
|
|
184
|
+
doom_loop_threshold: doom_loop_threshold, mcp_servers: mcp_servers,
|
|
185
|
+
mcp: mcp, tools: tools, config: config
|
|
186
|
+
)
|
|
203
187
|
|
|
204
|
-
|
|
205
|
-
|
|
188
|
+
extract_config_ivars
|
|
189
|
+
initialize_runtime_state
|
|
190
|
+
initialize_memory
|
|
191
|
+
configure_learning(learn: learn, learn_domain: learn_domain, store_path: store_path)
|
|
206
192
|
|
|
207
|
-
|
|
208
|
-
resolved_model = @config.model ||
|
|
209
|
-
chat_kwargs
|
|
193
|
+
lab_config = RobotLab.config
|
|
194
|
+
resolved_model = @config.model || lab_config.ruby_llm.model
|
|
195
|
+
chat_kwargs = { model: resolved_model }
|
|
210
196
|
|
|
211
|
-
#
|
|
212
|
-
# RubyLLM auto-sets assume_model_exists for local providers when
|
|
213
|
-
# provider is specified.
|
|
197
|
+
# RubyLLM auto-sets assume_model_exists for local providers when provider is specified.
|
|
214
198
|
@provider = provider
|
|
215
199
|
if @provider
|
|
216
200
|
chat_kwargs[:provider] = @provider
|
|
217
201
|
chat_kwargs[:assume_model_exists] = true
|
|
218
202
|
end
|
|
219
203
|
|
|
220
|
-
# Create the persistent chat via Agent's initialize
|
|
221
204
|
super(chat: nil, **chat_kwargs)
|
|
222
205
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
all_skill_ids = Array(@skills)
|
|
228
|
-
if @template
|
|
229
|
-
parsed_main = PM.parse(@template)
|
|
230
|
-
fm_skills = extract_skills_from_metadata(parsed_main.metadata)
|
|
231
|
-
all_skill_ids = all_skill_ids + fm_skills
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
if all_skill_ids.any?
|
|
235
|
-
# Skills path: expand skills, merge config, concatenate bodies
|
|
236
|
-
apply_skills_and_template_to_chat(all_skill_ids, context)
|
|
237
|
-
elsif @template
|
|
238
|
-
# Standard path: single template, no skills
|
|
239
|
-
apply_template_to_chat(context)
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
@chat.with_instructions(@system_prompt) if @system_prompt
|
|
243
|
-
|
|
244
|
-
# Constructor params override template front matter (use config values)
|
|
245
|
-
@chat.with_temperature(@config.temperature) if @config.temperature
|
|
246
|
-
|
|
247
|
-
# These parameters don't have dedicated with_* methods on Chat;
|
|
248
|
-
# pass them through with_params.
|
|
249
|
-
extra_params = {
|
|
250
|
-
top_p: @config.top_p,
|
|
251
|
-
top_k: @config.top_k,
|
|
252
|
-
max_tokens: @config.max_tokens,
|
|
253
|
-
presence_penalty: @config.presence_penalty,
|
|
254
|
-
frequency_penalty: @config.frequency_penalty,
|
|
255
|
-
stop: @config.stop
|
|
256
|
-
}.compact
|
|
257
|
-
@chat.with_params(**extra_params) if extra_params.any?
|
|
258
|
-
|
|
259
|
-
# Apply callbacks
|
|
260
|
-
@chat.on_tool_call(&@on_tool_call) if @on_tool_call
|
|
261
|
-
@chat.on_tool_result(&@on_tool_result) if @on_tool_result
|
|
262
|
-
|
|
263
|
-
# Set up bus channel if a bus was provided
|
|
264
|
-
setup_bus_channel if @bus
|
|
206
|
+
apply_template
|
|
207
|
+
apply_system_prompt
|
|
208
|
+
apply_chat_params
|
|
209
|
+
register_chat_callbacks
|
|
265
210
|
end
|
|
266
211
|
|
|
267
|
-
|
|
268
212
|
# Returns the model identifier
|
|
269
213
|
#
|
|
270
214
|
# @return [String, nil] the LLM model ID string
|
|
@@ -275,20 +219,6 @@ module RobotLab
|
|
|
275
219
|
m.respond_to?(:id) ? m.id : m.to_s
|
|
276
220
|
end
|
|
277
221
|
|
|
278
|
-
# Dynamically delegate all with_* methods from @chat, returning self for chaining.
|
|
279
|
-
# Discovered from the actual Chat class to avoid maintenance sync issues.
|
|
280
|
-
private def define_chat_delegators
|
|
281
|
-
@chat.class.public_instance_methods(false)
|
|
282
|
-
.select { |m| m.start_with?('with_') }
|
|
283
|
-
.each do |method_name|
|
|
284
|
-
define_singleton_method(method_name) do |*args, **kwargs, &block|
|
|
285
|
-
@chat.public_send(method_name, *args, **kwargs, &block)
|
|
286
|
-
self
|
|
287
|
-
end
|
|
288
|
-
end
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
|
|
292
222
|
# Send a message and get a response, with Robot's extended capabilities
|
|
293
223
|
#
|
|
294
224
|
# @param message [String] the user message
|
|
@@ -301,74 +231,27 @@ module RobotLab
|
|
|
301
231
|
# @return [RobotResult]
|
|
302
232
|
def run(message = nil, network: nil, network_memory: nil, network_config: nil,
|
|
303
233
|
memory: nil, mcp: :none, tools: :none, **kwargs, &block)
|
|
304
|
-
|
|
305
|
-
run_memory = resolve_active_memory(network: network, network_memory: network_memory)
|
|
306
|
-
|
|
307
|
-
# Merge runtime memory if provided
|
|
308
|
-
case memory
|
|
309
|
-
when Memory
|
|
310
|
-
run_memory = memory
|
|
311
|
-
when Hash
|
|
312
|
-
run_memory.merge!(memory)
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
# Set current_writer so memory notifications know who wrote the value
|
|
234
|
+
run_memory = resolve_run_memory(memory, network: network, network_memory: network_memory)
|
|
316
235
|
previous_writer = run_memory.current_writer
|
|
317
236
|
run_memory.current_writer = @name
|
|
318
237
|
|
|
319
238
|
begin
|
|
320
|
-
# Resolve hierarchical MCP and tools configuration
|
|
321
|
-
resolved_mcp = resolve_mcp_hierarchy(mcp, network: network, network_config: network_config)
|
|
322
|
-
resolved_tools = resolve_tools_hierarchy(tools, network: network, network_config: network_config)
|
|
323
|
-
|
|
324
|
-
# Filter MCP servers by semantic relevance when discovery is enabled.
|
|
325
|
-
# Only applies on the first run (before @mcp_initialized) so connections
|
|
326
|
-
# are not torn down mid-conversation.
|
|
327
|
-
if @mcp_discovery && !@mcp_initialized && resolved_mcp.is_a?(Array)
|
|
328
|
-
resolved_mcp = MCP::ServerDiscovery.select(message.to_s, from: resolved_mcp)
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
# Initialize or update MCP clients based on resolved config
|
|
332
|
-
ensure_mcp_clients(resolved_mcp)
|
|
333
|
-
|
|
334
|
-
# Apply filtered tools to the persistent chat
|
|
335
|
-
filtered = filtered_tools(resolved_tools)
|
|
336
|
-
@chat.with_tools(*filtered) if filtered.any?
|
|
337
|
-
|
|
338
|
-
# Re-render template with run-time context merged into build-time context.
|
|
339
|
-
# Template parameters (e.g. customer: null) may require values that are
|
|
340
|
-
# only available at run time — the robot gathers them before rendering.
|
|
341
239
|
run_context = kwargs.except(:with)
|
|
240
|
+
prepare_tools(message: message, mcp: mcp, tools: tools,
|
|
241
|
+
network: network, network_config: network_config)
|
|
342
242
|
rerender_template(run_context) if @template && run_context.any?
|
|
343
|
-
|
|
344
|
-
# Prepend accumulated learnings to the user message
|
|
345
|
-
effective_message = inject_learnings(message)
|
|
346
|
-
|
|
347
|
-
# Install circuit breaker for this run if max_tool_rounds is configured
|
|
348
|
-
install_circuit_breaker if @config.max_tool_rounds
|
|
349
|
-
|
|
350
|
-
# Delegate to Agent's ask (which calls @chat.ask)
|
|
351
|
-
ask_kwargs = kwargs.slice(:with)
|
|
352
|
-
streaming = effective_streaming_block(block)
|
|
353
|
-
response = ask(effective_message, **ask_kwargs, &streaming)
|
|
354
|
-
|
|
243
|
+
response = invoke_ask(message: message, kwargs: kwargs, block: block)
|
|
355
244
|
result = build_result(response, run_memory)
|
|
356
|
-
|
|
357
|
-
# Enforce token budget if configured
|
|
358
|
-
budget = @config.token_budget
|
|
359
|
-
if budget && @total_input_tokens + @total_output_tokens > budget
|
|
360
|
-
raise InferenceError,
|
|
361
|
-
"Token budget exceeded: #{@total_input_tokens + @total_output_tokens} tokens used, budget is #{budget}"
|
|
362
|
-
end
|
|
363
|
-
|
|
245
|
+
enforce_token_budget!
|
|
364
246
|
result
|
|
365
247
|
ensure
|
|
248
|
+
remove_doom_loop_detection
|
|
366
249
|
restore_tool_call_callback if @config.max_tool_rounds
|
|
250
|
+
run_reflector if @durable_store
|
|
367
251
|
run_memory.current_writer = previous_writer
|
|
368
252
|
end
|
|
369
253
|
end
|
|
370
254
|
|
|
371
|
-
|
|
372
255
|
# Reconfigure the robot for a new context
|
|
373
256
|
#
|
|
374
257
|
# @param template [Symbol, nil] new template to apply
|
|
@@ -396,7 +279,6 @@ module RobotLab
|
|
|
396
279
|
self
|
|
397
280
|
end
|
|
398
281
|
|
|
399
|
-
|
|
400
282
|
# SimpleFlow step interface
|
|
401
283
|
#
|
|
402
284
|
# @param result [SimpleFlow::Result] incoming result from previous step
|
|
@@ -429,7 +311,6 @@ module RobotLab
|
|
|
429
311
|
.continue(error_result)
|
|
430
312
|
end
|
|
431
313
|
|
|
432
|
-
|
|
433
314
|
# Reset the robot's inherent memory
|
|
434
315
|
#
|
|
435
316
|
# @return [self]
|
|
@@ -438,7 +319,6 @@ module RobotLab
|
|
|
438
319
|
self
|
|
439
320
|
end
|
|
440
321
|
|
|
441
|
-
|
|
442
322
|
# Eagerly connect to configured MCP servers and discover tools.
|
|
443
323
|
# Normally MCP connections are lazy (established on first run).
|
|
444
324
|
# Call this to connect early, e.g. to display connection status at startup.
|
|
@@ -468,7 +348,6 @@ module RobotLab
|
|
|
468
348
|
self
|
|
469
349
|
end
|
|
470
350
|
|
|
471
|
-
|
|
472
351
|
# --- Public APIs for external MCP and history management (A4) ---
|
|
473
352
|
|
|
474
353
|
# Inject pre-connected MCP clients and their tools into this robot.
|
|
@@ -508,10 +387,10 @@ module RobotLab
|
|
|
508
387
|
def clear_messages(keep_system: true)
|
|
509
388
|
if keep_system
|
|
510
389
|
system_msg = @chat.messages.find { |m| m.role == :system }
|
|
511
|
-
@chat.
|
|
390
|
+
@chat.reset_messages!
|
|
512
391
|
@chat.add_message(system_msg) if system_msg
|
|
513
392
|
else
|
|
514
|
-
@chat.
|
|
393
|
+
@chat.reset_messages!
|
|
515
394
|
end
|
|
516
395
|
self
|
|
517
396
|
end
|
|
@@ -521,7 +400,8 @@ module RobotLab
|
|
|
521
400
|
# @param messages [Array<RubyLLM::Message>] the messages to restore
|
|
522
401
|
# @return [self]
|
|
523
402
|
def replace_messages(messages)
|
|
524
|
-
@chat.
|
|
403
|
+
@chat.reset_messages!
|
|
404
|
+
messages.each { |m| @chat.add_message(m) }
|
|
525
405
|
self
|
|
526
406
|
end
|
|
527
407
|
|
|
@@ -583,14 +463,14 @@ module RobotLab
|
|
|
583
463
|
# @param kwargs [Hash] additional keyword args forwarded to Robot#run
|
|
584
464
|
# @return [RobotResult] when async: false
|
|
585
465
|
# @return [DelegationFuture] when async: true
|
|
586
|
-
def delegate(to:, task:, async: false, **
|
|
466
|
+
def delegate(to:, task:, async: false, **)
|
|
587
467
|
if async
|
|
588
468
|
future = DelegationFuture.new(robot_name: to.name, delegated_by: @name)
|
|
589
469
|
delegator_name = @name
|
|
590
470
|
|
|
591
471
|
Thread.new do
|
|
592
472
|
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
593
|
-
result = to.run(task, **
|
|
473
|
+
result = to.run(task, **)
|
|
594
474
|
result.duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
|
|
595
475
|
result.delegated_by = delegator_name
|
|
596
476
|
future.resolve!(result)
|
|
@@ -601,7 +481,7 @@ module RobotLab
|
|
|
601
481
|
future
|
|
602
482
|
else
|
|
603
483
|
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
604
|
-
result = to.run(task, **
|
|
484
|
+
result = to.run(task, **)
|
|
605
485
|
result.duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
|
|
606
486
|
result.delegated_by = @name
|
|
607
487
|
result
|
|
@@ -628,7 +508,6 @@ module RobotLab
|
|
|
628
508
|
@mcp_clients[server_name]
|
|
629
509
|
end
|
|
630
510
|
|
|
631
|
-
|
|
632
511
|
# Add a learning to this robot's accumulation store.
|
|
633
512
|
#
|
|
634
513
|
# Deduplicates by bidirectional substring matching: a new learning is
|
|
@@ -656,7 +535,6 @@ module RobotLab
|
|
|
656
535
|
self
|
|
657
536
|
end
|
|
658
537
|
|
|
659
|
-
|
|
660
538
|
# Reset cumulative token counters to zero.
|
|
661
539
|
#
|
|
662
540
|
# @return [self]
|
|
@@ -666,7 +544,6 @@ module RobotLab
|
|
|
666
544
|
self
|
|
667
545
|
end
|
|
668
546
|
|
|
669
|
-
|
|
670
547
|
# Converts the robot to a hash representation
|
|
671
548
|
#
|
|
672
549
|
# @return [Hash]
|
|
@@ -690,15 +567,189 @@ module RobotLab
|
|
|
690
567
|
|
|
691
568
|
private
|
|
692
569
|
|
|
693
|
-
|
|
570
|
+
def assign_identity_ivars(name:, template:, system_prompt:, context:, description:,
|
|
571
|
+
local_tools:, skills:, mcp_discovery:)
|
|
572
|
+
@name = name.to_s
|
|
573
|
+
@name_from_constructor = (name.to_s != "robot")
|
|
574
|
+
@template = template
|
|
575
|
+
@system_prompt = system_prompt
|
|
576
|
+
@build_context = context
|
|
577
|
+
@description = description
|
|
578
|
+
@local_tools = Array(local_tools)
|
|
579
|
+
@skills = skills ? Array(skills).map(&:to_sym) : nil
|
|
580
|
+
@expanded_skills = nil
|
|
581
|
+
@pending_agent_skills = []
|
|
582
|
+
@agent_skill_store = nil
|
|
583
|
+
@mcp_discovery = mcp_discovery
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
# Build RunConfig from explicit kwargs, merged on top of any passed-in config.
|
|
587
|
+
# Explicit constructor kwargs always win.
|
|
588
|
+
def build_effective_config(model:, temperature:, top_p:, top_k:, max_tokens:,
|
|
589
|
+
presence_penalty:, frequency_penalty:, stop:,
|
|
590
|
+
on_tool_call:, on_tool_result:, on_content:,
|
|
591
|
+
bus:, enable_cache:, max_tool_rounds:, token_budget:,
|
|
592
|
+
doom_loop_threshold:, mcp_servers:, mcp:, tools:, config:)
|
|
593
|
+
explicit_fields = {
|
|
594
|
+
model: model, temperature: temperature, top_p: top_p, top_k: top_k,
|
|
595
|
+
max_tokens: max_tokens, presence_penalty: presence_penalty,
|
|
596
|
+
frequency_penalty: frequency_penalty, stop: stop,
|
|
597
|
+
on_tool_call: on_tool_call, on_tool_result: on_tool_result,
|
|
598
|
+
on_content: on_content, bus: bus, enable_cache: enable_cache,
|
|
599
|
+
max_tool_rounds: max_tool_rounds, token_budget: token_budget,
|
|
600
|
+
doom_loop_threshold: doom_loop_threshold
|
|
601
|
+
}.compact
|
|
602
|
+
|
|
603
|
+
resolved_mcp = mcp_servers.any? ? mcp_servers : mcp
|
|
604
|
+
explicit_fields[:mcp] = resolved_mcp unless ToolConfig.none_value?(resolved_mcp)
|
|
605
|
+
explicit_fields[:tools] = tools unless ToolConfig.none_value?(tools)
|
|
606
|
+
|
|
607
|
+
explicit_config = RunConfig.new(**explicit_fields)
|
|
608
|
+
@config = config ? config.merge(explicit_config) : explicit_config
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def extract_config_ivars
|
|
612
|
+
@on_tool_call = @config.on_tool_call
|
|
613
|
+
@on_tool_result = @config.on_tool_result
|
|
614
|
+
@on_content = @config.on_content
|
|
615
|
+
@mcp_config = @config.mcp || :none
|
|
616
|
+
@tools_config = @config.tools || :none
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def initialize_runtime_state
|
|
620
|
+
@mcp_clients = {}
|
|
621
|
+
@mcp_tools = []
|
|
622
|
+
@mcp_initialized = false
|
|
623
|
+
@bus = @config.bus
|
|
624
|
+
@message_counter = 0
|
|
625
|
+
@outbox = {}
|
|
626
|
+
@message_handler = ->(_msg) {}
|
|
627
|
+
@bus_poller = nil
|
|
628
|
+
@private_bus_poller = nil
|
|
629
|
+
@bus_poller_group = :default
|
|
630
|
+
@total_input_tokens = 0
|
|
631
|
+
@total_output_tokens = 0
|
|
632
|
+
@learnings = []
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def initialize_memory
|
|
636
|
+
cache_enabled = @config.key?(:enable_cache) ? @config.enable_cache : true
|
|
637
|
+
@memory = Memory.new(enable_cache: cache_enabled)
|
|
638
|
+
persisted = @memory.get(:learnings)
|
|
639
|
+
@learnings = Array(persisted) if persisted
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def configure_learning(learn:, learn_domain:, store_path:)
|
|
643
|
+
return unless learn && RobotLab.extension_loaded?(:durable)
|
|
644
|
+
|
|
645
|
+
if learn_domain
|
|
646
|
+
setup_durable_learning(domain: learn_domain, store_path: store_path)
|
|
647
|
+
else
|
|
648
|
+
warn "[RobotLab] Robot '#{@name}': learn: true requires learn_domain: to be set. Durable learning disabled."
|
|
649
|
+
end
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def apply_template
|
|
653
|
+
define_chat_delegators
|
|
654
|
+
|
|
655
|
+
all_skill_ids = Array(@skills)
|
|
656
|
+
if @template
|
|
657
|
+
parsed_main = PM.parse(@template)
|
|
658
|
+
fm_skills = extract_skills_from_metadata(parsed_main.metadata)
|
|
659
|
+
all_skill_ids += fm_skills
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
if all_skill_ids.any?
|
|
663
|
+
apply_skills_and_template_to_chat(all_skill_ids, @build_context)
|
|
664
|
+
elsif @template
|
|
665
|
+
apply_template_to_chat(@build_context)
|
|
666
|
+
end
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def apply_system_prompt
|
|
670
|
+
@chat.with_instructions(@system_prompt) if @system_prompt
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
def apply_chat_params
|
|
674
|
+
@chat.with_temperature(@config.temperature) if @config.temperature
|
|
675
|
+
|
|
676
|
+
extra_params = {
|
|
677
|
+
top_p: @config.top_p, top_k: @config.top_k,
|
|
678
|
+
max_tokens: @config.max_tokens,
|
|
679
|
+
presence_penalty: @config.presence_penalty,
|
|
680
|
+
frequency_penalty: @config.frequency_penalty,
|
|
681
|
+
stop: @config.stop
|
|
682
|
+
}.compact
|
|
683
|
+
@chat.with_params(**extra_params) if extra_params.any?
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def register_chat_callbacks
|
|
687
|
+
@chat.on_tool_call(&@on_tool_call) if @on_tool_call
|
|
688
|
+
@chat.on_tool_result(&@on_tool_result) if @on_tool_result
|
|
689
|
+
setup_bus_channel if @bus
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# Dynamically delegate all with_* methods from @chat, returning self for chaining.
|
|
693
|
+
# Discovered from the actual Chat class to avoid maintenance sync issues.
|
|
694
|
+
def define_chat_delegators
|
|
695
|
+
@chat.class.public_instance_methods(false)
|
|
696
|
+
.select { |m| m.start_with?('with_') }
|
|
697
|
+
.each do |method_name|
|
|
698
|
+
define_singleton_method(method_name) do |*args, **kwargs, &block|
|
|
699
|
+
@chat.public_send(method_name, *args, **kwargs, &block)
|
|
700
|
+
self
|
|
701
|
+
end
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
|
|
694
705
|
def resolve_active_memory(network: nil, network_memory: nil)
|
|
695
706
|
network_memory || network&.memory || @memory
|
|
696
707
|
end
|
|
697
708
|
|
|
709
|
+
def resolve_run_memory(memory, network:, network_memory:)
|
|
710
|
+
run_memory = resolve_active_memory(network: network, network_memory: network_memory)
|
|
711
|
+
case memory
|
|
712
|
+
when Memory then memory
|
|
713
|
+
when Hash then run_memory.tap { |m| m.merge!(memory) }
|
|
714
|
+
else run_memory
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
def prepare_tools(message:, mcp:, tools:, network:, network_config:)
|
|
719
|
+
resolved_mcp = resolve_mcp_hierarchy(mcp, network: network, network_config: network_config)
|
|
720
|
+
resolved_tools = resolve_tools_hierarchy(tools, network: network, network_config: network_config)
|
|
721
|
+
|
|
722
|
+
if @mcp_discovery && !@mcp_initialized && resolved_mcp.is_a?(Array)
|
|
723
|
+
resolved_mcp = MCP::ServerDiscovery.select(message.to_s, from: resolved_mcp)
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
ensure_mcp_clients(resolved_mcp)
|
|
727
|
+
|
|
728
|
+
filtered = filtered_tools(resolved_tools)
|
|
729
|
+
@chat.with_tools(*filtered) if filtered.any?
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
def invoke_ask(message:, kwargs:, block:)
|
|
733
|
+
effective_message = inject_learnings(message)
|
|
734
|
+
maybe_compact
|
|
735
|
+
install_circuit_breaker if @config.max_tool_rounds
|
|
736
|
+
install_doom_loop_detection
|
|
737
|
+
ask_kwargs = kwargs.slice(:with)
|
|
738
|
+
streaming = effective_streaming_block(block)
|
|
739
|
+
ask(effective_message, **ask_kwargs, &streaming)
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def enforce_token_budget!
|
|
743
|
+
budget = @config.token_budget
|
|
744
|
+
return unless budget && @total_input_tokens + @total_output_tokens > budget
|
|
745
|
+
|
|
746
|
+
raise InferenceError,
|
|
747
|
+
"Token budget exceeded: #{@total_input_tokens + @total_output_tokens} tokens used, budget is #{budget}"
|
|
748
|
+
end
|
|
698
749
|
|
|
699
750
|
# Extract run context from SimpleFlow::Result
|
|
700
751
|
def extract_run_context(result)
|
|
701
|
-
run_params = result.context[:run_params] || {}
|
|
752
|
+
run_params = (result.context[:run_params] || {}).dup
|
|
702
753
|
|
|
703
754
|
# Extract robot-specific params
|
|
704
755
|
mcp = run_params.delete(:mcp) || :none
|
|
@@ -732,7 +783,6 @@ module RobotLab
|
|
|
732
783
|
merged
|
|
733
784
|
end
|
|
734
785
|
|
|
735
|
-
|
|
736
786
|
def build_result(response, _memory)
|
|
737
787
|
output = if response.respond_to?(:content) && response.content
|
|
738
788
|
[TextMessage.new(role: 'assistant', content: response.content)]
|
|
@@ -767,7 +817,6 @@ module RobotLab
|
|
|
767
817
|
)
|
|
768
818
|
end
|
|
769
819
|
|
|
770
|
-
|
|
771
820
|
def normalize_tool_calls(tool_calls)
|
|
772
821
|
return [] unless tool_calls
|
|
773
822
|
|
|
@@ -783,7 +832,6 @@ module RobotLab
|
|
|
783
832
|
end
|
|
784
833
|
end
|
|
785
834
|
|
|
786
|
-
|
|
787
835
|
# Merge the stored on_content callback with a runtime streaming block.
|
|
788
836
|
# If both exist, both fire (stored first, then runtime block).
|
|
789
837
|
#
|
|
@@ -794,15 +842,16 @@ module RobotLab
|
|
|
794
842
|
return runtime_block unless @on_content
|
|
795
843
|
|
|
796
844
|
stored = @on_content
|
|
797
|
-
proc { |chunk|
|
|
845
|
+
proc { |chunk|
|
|
846
|
+
stored.call(chunk)
|
|
847
|
+
runtime_block.call(chunk)
|
|
848
|
+
}
|
|
798
849
|
end
|
|
799
850
|
|
|
800
|
-
|
|
801
851
|
def all_tools
|
|
802
852
|
@local_tools + @mcp_tools
|
|
803
853
|
end
|
|
804
854
|
|
|
805
|
-
|
|
806
855
|
def filtered_tools(allowed_names)
|
|
807
856
|
available = all_tools
|
|
808
857
|
return available if allowed_names.empty?
|
|
@@ -810,7 +859,6 @@ module RobotLab
|
|
|
810
859
|
ToolConfig.filter_tools(available, allowed_names: allowed_names)
|
|
811
860
|
end
|
|
812
861
|
|
|
813
|
-
|
|
814
862
|
# Prepend accumulated learnings to a user message when learnings exist.
|
|
815
863
|
def inject_learnings(message)
|
|
816
864
|
return message if @learnings.empty? || message.nil?
|
|
@@ -821,6 +869,76 @@ module RobotLab
|
|
|
821
869
|
"#{learning_block}#{message}"
|
|
822
870
|
end
|
|
823
871
|
|
|
872
|
+
# Install per-run doom loop detection on @chat's execute_tool.
|
|
873
|
+
# Tracks tool call names; when a consecutive or cyclic repetition exceeds
|
|
874
|
+
# the threshold, embeds a self-correction warning in the tool result so the
|
|
875
|
+
# LLM can change strategy without requiring an external circuit breaker.
|
|
876
|
+
def install_doom_loop_detection
|
|
877
|
+
threshold = @config.doom_loop_threshold || DoomLoopDetector::DEFAULT_THRESHOLD
|
|
878
|
+
detector = DoomLoopDetector.new(threshold: threshold)
|
|
879
|
+
|
|
880
|
+
@chat.define_singleton_method(:execute_tool) do |tool_call|
|
|
881
|
+
result = super(tool_call)
|
|
882
|
+
detector.track(tool_call.name)
|
|
883
|
+
|
|
884
|
+
if detector.doom_loop?
|
|
885
|
+
warning = detector.warning_message
|
|
886
|
+
detector.reset
|
|
887
|
+
case result
|
|
888
|
+
when Hash then result.merge(_doom_loop_warning: warning)
|
|
889
|
+
when String then "#{result}\n\n⚠️ #{warning}"
|
|
890
|
+
else result
|
|
891
|
+
end
|
|
892
|
+
else
|
|
893
|
+
result
|
|
894
|
+
end
|
|
895
|
+
end
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
# Remove the doom loop detection singleton method from @chat.
|
|
899
|
+
def remove_doom_loop_detection
|
|
900
|
+
sc = @chat.singleton_class
|
|
901
|
+
sc.remove_method(:execute_tool) if sc.method_defined?(:execute_tool)
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
# Compact conversation history before an ask() call if auto_compact is set.
|
|
905
|
+
#
|
|
906
|
+
# :none (default) — no-op
|
|
907
|
+
# :context_window — compress when estimated tokens exceed compact_threshold
|
|
908
|
+
# fraction of the model's context window (default 80%)
|
|
909
|
+
# Proc — called with self; application owns the decision and strategy
|
|
910
|
+
def maybe_compact
|
|
911
|
+
return if @chat.messages.empty?
|
|
912
|
+
|
|
913
|
+
compact = @config.auto_compact
|
|
914
|
+
return if compact.nil? || compact == :none
|
|
915
|
+
|
|
916
|
+
case compact
|
|
917
|
+
when :context_window
|
|
918
|
+
compact_if_over_context_window
|
|
919
|
+
when Proc
|
|
920
|
+
compact.call(self)
|
|
921
|
+
end
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
def compact_if_over_context_window
|
|
925
|
+
threshold = (@config.compact_threshold || 0.80).to_f
|
|
926
|
+
estimated_tok = @chat.messages.sum { |m| m.content.to_s.length } / 4
|
|
927
|
+
|
|
928
|
+
window = begin
|
|
929
|
+
RubyLLM.models.find(model)&.context_window || 200_000
|
|
930
|
+
rescue StandardError
|
|
931
|
+
200_000
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
return if estimated_tok < window * threshold
|
|
935
|
+
|
|
936
|
+
begin
|
|
937
|
+
compress_history
|
|
938
|
+
rescue DependencyError => e
|
|
939
|
+
RobotLab.config.logger.warn("[#{@name}] auto_compact: #{e.message}; skipping compaction")
|
|
940
|
+
end
|
|
941
|
+
end
|
|
824
942
|
|
|
825
943
|
# Install a per-run circuit breaker on the chat's on_tool_call hook.
|
|
826
944
|
# Raises ToolLoopError if tool calls exceed @config.max_tool_rounds.
|
|
@@ -841,7 +959,6 @@ module RobotLab
|
|
|
841
959
|
end
|
|
842
960
|
end
|
|
843
961
|
|
|
844
|
-
|
|
845
962
|
# Restore the original on_tool_call callback after a circuit-breaker run.
|
|
846
963
|
def restore_tool_call_callback
|
|
847
964
|
@chat.on_tool_call(&@on_tool_call)
|