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/ask_user.rb
CHANGED
|
@@ -65,11 +65,11 @@ module RobotLab
|
|
|
65
65
|
private
|
|
66
66
|
|
|
67
67
|
def input_io
|
|
68
|
-
robot
|
|
68
|
+
robot.respond_to?(:input) && robot.input ? robot.input : $stdin
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def output_io
|
|
72
|
-
robot
|
|
72
|
+
robot.respond_to?(:output) && robot.output ? robot.output : $stdout
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
end
|
data/lib/robot_lab/bus_poller.rb
CHANGED
|
@@ -115,7 +115,16 @@ module RobotLab
|
|
|
115
115
|
# Process one delivery, then drain any queued deliveries for the robot.
|
|
116
116
|
def process_and_drain(robot, delivery)
|
|
117
117
|
robot.send(:process_delivery, delivery)
|
|
118
|
+
drain_queued_deliveries(robot)
|
|
119
|
+
rescue BusError => e
|
|
120
|
+
release_robot(robot)
|
|
121
|
+
RobotLab.config.logger.warn("BusPoller: delivery error: #{e.message}")
|
|
122
|
+
rescue => e
|
|
123
|
+
release_robot(robot)
|
|
124
|
+
RobotLab.config.logger.warn("BusPoller: unexpected error: #{e.message}")
|
|
125
|
+
end
|
|
118
126
|
|
|
127
|
+
def drain_queued_deliveries(robot)
|
|
119
128
|
loop do
|
|
120
129
|
next_delivery = @mutex.synchronize do
|
|
121
130
|
name = robot.name
|
|
@@ -138,12 +147,10 @@ module RobotLab
|
|
|
138
147
|
RobotLab.config.logger.warn("BusPoller: delivery error: #{e.message}")
|
|
139
148
|
end
|
|
140
149
|
end
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
rescue => e
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def release_robot(robot)
|
|
145
153
|
@mutex.synchronize { @robot_busy[robot.name] = false }
|
|
146
|
-
RobotLab.config.logger.warn("BusPoller: unexpected error: #{e.message}")
|
|
147
154
|
end
|
|
148
155
|
end
|
|
149
156
|
end
|
data/lib/robot_lab/config.rb
CHANGED
|
@@ -43,7 +43,6 @@ module RobotLab
|
|
|
43
43
|
@logger ||= default_logger
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
|
|
47
46
|
# Apply RubyLLM configuration after loading.
|
|
48
47
|
#
|
|
49
48
|
# This method should be called after initialization to configure
|
|
@@ -56,7 +55,6 @@ module RobotLab
|
|
|
56
55
|
apply_prompt_manager!
|
|
57
56
|
end
|
|
58
57
|
|
|
59
|
-
|
|
60
58
|
# Apply all RubyLLM settings from the ruby_llm configuration section.
|
|
61
59
|
#
|
|
62
60
|
# @return [void]
|
|
@@ -100,7 +98,6 @@ module RobotLab
|
|
|
100
98
|
set_if_present(c, :vertexai_location, :vertexai_location, 'GOOGLE_CLOUD_LOCATION')
|
|
101
99
|
end
|
|
102
100
|
|
|
103
|
-
|
|
104
101
|
def apply_provider_endpoints(c)
|
|
105
102
|
c.openai_api_base = ruby_llm.openai_api_base if ruby_llm.openai_api_base
|
|
106
103
|
c.gemini_api_base = ruby_llm.gemini_api_base if ruby_llm.gemini_api_base
|
|
@@ -109,14 +106,12 @@ module RobotLab
|
|
|
109
106
|
c.xai_api_base = ruby_llm.xai_api_base if ruby_llm.xai_api_base
|
|
110
107
|
end
|
|
111
108
|
|
|
112
|
-
|
|
113
109
|
def apply_openai_options(c)
|
|
114
110
|
c.openai_organization_id = ruby_llm.openai_organization_id if ruby_llm.openai_organization_id
|
|
115
111
|
c.openai_project_id = ruby_llm.openai_project_id if ruby_llm.openai_project_id
|
|
116
112
|
c.openai_use_system_role = ruby_llm.openai_use_system_role unless ruby_llm.openai_use_system_role.nil?
|
|
117
113
|
end
|
|
118
114
|
|
|
119
|
-
|
|
120
115
|
def apply_default_models(c)
|
|
121
116
|
c.default_model = ruby_llm.default_model if ruby_llm.default_model
|
|
122
117
|
c.default_embedding_model = ruby_llm.default_embedding_model if ruby_llm.default_embedding_model
|
|
@@ -124,7 +119,6 @@ module RobotLab
|
|
|
124
119
|
c.default_moderation_model = ruby_llm.default_moderation_model if ruby_llm.default_moderation_model
|
|
125
120
|
end
|
|
126
121
|
|
|
127
|
-
|
|
128
122
|
def apply_connection_settings(c)
|
|
129
123
|
c.request_timeout = ruby_llm.request_timeout if ruby_llm.request_timeout
|
|
130
124
|
c.max_retries = ruby_llm.max_retries if ruby_llm.max_retries
|
|
@@ -134,14 +128,12 @@ module RobotLab
|
|
|
134
128
|
c.http_proxy = ruby_llm.http_proxy if ruby_llm.http_proxy
|
|
135
129
|
end
|
|
136
130
|
|
|
137
|
-
|
|
138
131
|
def apply_logging_options(c)
|
|
139
132
|
c.log_file = ruby_llm.log_file if ruby_llm.log_file
|
|
140
133
|
c.log_level = ruby_llm.log_level if ruby_llm.log_level
|
|
141
134
|
c.log_stream_debug = ruby_llm.log_stream_debug unless ruby_llm.log_stream_debug.nil?
|
|
142
135
|
end
|
|
143
136
|
|
|
144
|
-
|
|
145
137
|
def apply_prompt_manager!
|
|
146
138
|
path = resolved_template_path
|
|
147
139
|
return unless path
|
|
@@ -151,15 +143,13 @@ module RobotLab
|
|
|
151
143
|
end
|
|
152
144
|
end
|
|
153
145
|
|
|
154
|
-
|
|
155
146
|
# Set a RubyLLM config attribute from config value or standard env var.
|
|
156
147
|
# Only sets when a non-nil value is found, to avoid overwriting defaults.
|
|
157
148
|
def set_if_present(c, setter, config_key, env_var)
|
|
158
|
-
value = ruby_llm.public_send(config_key) || ENV
|
|
149
|
+
value = ruby_llm.public_send(config_key) || ENV.fetch(env_var, nil)
|
|
159
150
|
c.public_send(:"#{setter}=", value) if value
|
|
160
151
|
end
|
|
161
152
|
|
|
162
|
-
|
|
163
153
|
def resolved_template_path
|
|
164
154
|
return template_path if template_path
|
|
165
155
|
|
|
@@ -170,7 +160,6 @@ module RobotLab
|
|
|
170
160
|
end
|
|
171
161
|
end
|
|
172
162
|
|
|
173
|
-
|
|
174
163
|
def default_logger
|
|
175
164
|
if defined?(Rails) && Rails.respond_to?(:logger)
|
|
176
165
|
Rails.logger
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
# Detects and interrupts tool-call doom loops inside the LLM agent loop.
|
|
5
|
+
#
|
|
6
|
+
# A doom loop occurs when the LLM repeatedly calls the same tool(s) in a
|
|
7
|
+
# cycle — either identical consecutive calls (A, A, A) or a repeating
|
|
8
|
+
# multi-step pattern (A, B, C, A, B, C, A, B, C).
|
|
9
|
+
#
|
|
10
|
+
# When a doom loop is detected the warning is embedded in the tool result
|
|
11
|
+
# so the LLM sees it in its next context window and can self-correct.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# detector = DoomLoopDetector.new(threshold: 3)
|
|
15
|
+
# detector.track("search")
|
|
16
|
+
# detector.track("search")
|
|
17
|
+
# detector.track("search")
|
|
18
|
+
# detector.doom_loop? # => true
|
|
19
|
+
# detector.warning_message
|
|
20
|
+
# # => "Tool 'search' has been called 3 times consecutively..."
|
|
21
|
+
class DoomLoopDetector
|
|
22
|
+
DEFAULT_THRESHOLD = 3
|
|
23
|
+
MAX_PERIOD = 10
|
|
24
|
+
|
|
25
|
+
attr_reader :sequence
|
|
26
|
+
|
|
27
|
+
def initialize(threshold: DEFAULT_THRESHOLD)
|
|
28
|
+
@threshold = threshold
|
|
29
|
+
@sequence = []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def track(tool_name)
|
|
33
|
+
@sequence << tool_name.to_s
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def doom_loop?
|
|
37
|
+
seq = @sequence
|
|
38
|
+
return false if seq.length < @threshold
|
|
39
|
+
|
|
40
|
+
# Consecutive identical calls: A, A, A
|
|
41
|
+
tail = seq.last(@threshold)
|
|
42
|
+
return true if tail.uniq.length == 1
|
|
43
|
+
|
|
44
|
+
# Cyclic multi-step patterns: A,B,C, A,B,C, A,B,C
|
|
45
|
+
max_period = [MAX_PERIOD, seq.length / @threshold].min
|
|
46
|
+
(2..max_period).each do |period|
|
|
47
|
+
window = @threshold * period
|
|
48
|
+
next if seq.length < window
|
|
49
|
+
|
|
50
|
+
chunk = seq.last(window)
|
|
51
|
+
pattern = chunk.first(period)
|
|
52
|
+
return true if chunk == pattern * @threshold
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def reset
|
|
59
|
+
@sequence = []
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def warning_message
|
|
63
|
+
seq = @sequence
|
|
64
|
+
return "" if seq.empty?
|
|
65
|
+
|
|
66
|
+
period = detect_period(seq)
|
|
67
|
+
if period == 1
|
|
68
|
+
tool = seq.last
|
|
69
|
+
"Tool '#{tool}' has been called #{@threshold}+ times consecutively with " \
|
|
70
|
+
"no progress. You appear to be stuck in a loop. Try a fundamentally " \
|
|
71
|
+
"different approach, use a different tool, or ask for clarification."
|
|
72
|
+
else
|
|
73
|
+
pattern = seq.last(period).join(" → ")
|
|
74
|
+
"Tool call pattern [#{pattern}] has repeated #{@threshold}+ times. " \
|
|
75
|
+
"You appear to be stuck in a loop. Try a fundamentally different " \
|
|
76
|
+
"approach, use a different tool, or ask for clarification."
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def detect_period(seq)
|
|
83
|
+
return 1 if seq.last(@threshold).uniq.length == 1
|
|
84
|
+
|
|
85
|
+
max_period = [MAX_PERIOD, seq.length / @threshold].min
|
|
86
|
+
(2..max_period).each do |period|
|
|
87
|
+
window = @threshold * period
|
|
88
|
+
next if seq.length < window
|
|
89
|
+
|
|
90
|
+
chunk = seq.last(window)
|
|
91
|
+
pattern = chunk.first(period)
|
|
92
|
+
return period if chunk == pattern * @threshold
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
1
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -124,13 +124,11 @@ module RobotLab
|
|
|
124
124
|
|
|
125
125
|
if score >= @keep_threshold
|
|
126
126
|
:keep
|
|
127
|
-
elsif score < @drop_threshold
|
|
127
|
+
elsif score < @drop_threshold || !@summarizer
|
|
128
128
|
:drop
|
|
129
|
-
|
|
129
|
+
else
|
|
130
130
|
summary = @summarizer.call(text).to_s.strip
|
|
131
131
|
summary.empty? ? :drop : summary
|
|
132
|
-
else
|
|
133
|
-
:drop
|
|
134
132
|
end
|
|
135
133
|
end
|
|
136
134
|
|
|
@@ -141,10 +139,7 @@ module RobotLab
|
|
|
141
139
|
@messages.each_with_index do |msg, idx|
|
|
142
140
|
action = actions[idx]
|
|
143
141
|
|
|
144
|
-
if action.nil?
|
|
145
|
-
# Pinned or recent: always include
|
|
146
|
-
result << msg
|
|
147
|
-
elsif action == :keep
|
|
142
|
+
if action.nil? || action == :keep
|
|
148
143
|
result << msg
|
|
149
144
|
elsif action == :drop
|
|
150
145
|
# Omit entirely
|
|
@@ -166,7 +161,7 @@ module RobotLab
|
|
|
166
161
|
role = msg.role
|
|
167
162
|
|
|
168
163
|
return true if role == :system
|
|
169
|
-
return true if
|
|
164
|
+
return true if %i[tool tool_result].include?(role)
|
|
170
165
|
|
|
171
166
|
# Assistant tool-call dispatcher: content is nil or blank
|
|
172
167
|
if role == :assistant
|
|
@@ -185,7 +180,6 @@ module RobotLab
|
|
|
185
180
|
case content
|
|
186
181
|
when String then content
|
|
187
182
|
when Array then content.filter_map { |p| p[:text] || p["text"] }.join(" ")
|
|
188
|
-
else nil
|
|
189
183
|
end
|
|
190
184
|
end
|
|
191
185
|
|
data/lib/robot_lab/mcp/client.rb
CHANGED
|
@@ -141,8 +141,7 @@ module RobotLab
|
|
|
141
141
|
#
|
|
142
142
|
def get_prompt(name, arguments = {})
|
|
143
143
|
ensure_connected!
|
|
144
|
-
|
|
145
|
-
response
|
|
144
|
+
request(method: "prompts/get", params: { name: name, arguments: arguments })
|
|
146
145
|
end
|
|
147
146
|
|
|
148
147
|
# Checks if the client is connected to the server.
|
|
@@ -145,15 +145,15 @@ module RobotLab
|
|
|
145
145
|
loop do
|
|
146
146
|
ios = @mutex.synchronize { @clients.keys.reject(&:closed?) }
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
if ios.empty?
|
|
149
|
+
sleep POLL_INTERVAL
|
|
150
|
+
else
|
|
149
151
|
begin
|
|
150
152
|
ready, = IO.select(ios, nil, nil, POLL_INTERVAL)
|
|
151
153
|
dispatch(ready) if ready
|
|
152
154
|
rescue Errno::EBADF
|
|
153
155
|
# A pipe was closed between the reject and IO.select — harmless, loop again
|
|
154
156
|
end
|
|
155
|
-
else
|
|
156
|
-
sleep POLL_INTERVAL
|
|
157
157
|
end
|
|
158
158
|
|
|
159
159
|
break unless @mutex.synchronize { @running }
|
data/lib/robot_lab/mcp/server.rb
CHANGED
data/lib/robot_lab/memory.rb
CHANGED
|
@@ -89,7 +89,8 @@ module RobotLab
|
|
|
89
89
|
#
|
|
90
90
|
# @example Network-owned memory
|
|
91
91
|
# Memory.new(network_name: "support_pipeline")
|
|
92
|
-
def initialize(data: {}, results: [], messages: [], session_id: nil, backend: :auto, enable_cache: true,
|
|
92
|
+
def initialize(data: {}, results: [], messages: [], session_id: nil, backend: :auto, enable_cache: true,
|
|
93
|
+
network_name: nil)
|
|
93
94
|
@backend = select_backend(backend)
|
|
94
95
|
@mutex = Mutex.new
|
|
95
96
|
@enable_cache = enable_cache
|
|
@@ -104,7 +105,7 @@ module RobotLab
|
|
|
104
105
|
set_internal(:cache, @enable_cache ? RubyLLM::SemanticCache : nil)
|
|
105
106
|
|
|
106
107
|
# Data proxy for method-style access
|
|
107
|
-
@
|
|
108
|
+
@data = nil
|
|
108
109
|
|
|
109
110
|
# Reactive infrastructure
|
|
110
111
|
@subscriptions = Hash.new { |h, k| h[k] = [] }
|
|
@@ -149,7 +150,7 @@ module RobotLab
|
|
|
149
150
|
# Reserved keys have special handling (no notifications)
|
|
150
151
|
case key
|
|
151
152
|
when :data
|
|
152
|
-
@
|
|
153
|
+
@data = nil # Reset proxy
|
|
153
154
|
set_internal(:data, value.is_a?(Hash) ? value.transform_keys(&:to_sym) : value)
|
|
154
155
|
when :results
|
|
155
156
|
set_internal(:results, Array(value))
|
|
@@ -164,8 +165,6 @@ module RobotLab
|
|
|
164
165
|
# Non-reserved keys use reactive set
|
|
165
166
|
set(key, value)
|
|
166
167
|
end
|
|
167
|
-
|
|
168
|
-
value
|
|
169
168
|
end
|
|
170
169
|
|
|
171
170
|
# Access runtime data through StateProxy
|
|
@@ -173,7 +172,7 @@ module RobotLab
|
|
|
173
172
|
# @return [StateProxy] proxy for method-style data access
|
|
174
173
|
#
|
|
175
174
|
def data
|
|
176
|
-
@
|
|
175
|
+
@data ||= StateProxy.new(get_internal(:data) || {})
|
|
177
176
|
end
|
|
178
177
|
|
|
179
178
|
# Get copy of results (immutable access)
|
|
@@ -207,7 +206,6 @@ module RobotLab
|
|
|
207
206
|
#
|
|
208
207
|
def session_id=(id)
|
|
209
208
|
set_internal(:session_id, id)
|
|
210
|
-
self
|
|
211
209
|
end
|
|
212
210
|
|
|
213
211
|
# Get the semantic cache module
|
|
@@ -450,16 +448,14 @@ module RobotLab
|
|
|
450
448
|
# hits.each { |h| puts "#{h[:key]} (#{h[:score].round(3)}): #{h[:text][0..80]}" }
|
|
451
449
|
#
|
|
452
450
|
def search_documents(query, limit: 5)
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
@document_store.search(query, limit: limit)
|
|
451
|
+
document_store.search(query, limit: limit)
|
|
456
452
|
end
|
|
457
453
|
|
|
458
454
|
# Keys of all documents stored in the embedded document store.
|
|
459
455
|
#
|
|
460
456
|
# @return [Array<Symbol>]
|
|
461
457
|
def document_keys
|
|
462
|
-
|
|
458
|
+
document_store.keys
|
|
463
459
|
end
|
|
464
460
|
|
|
465
461
|
# Remove a document from the store.
|
|
@@ -467,7 +463,7 @@ module RobotLab
|
|
|
467
463
|
# @param key [Symbol, String]
|
|
468
464
|
# @return [self]
|
|
469
465
|
def delete_document(key)
|
|
470
|
-
|
|
466
|
+
document_store.delete(key)
|
|
471
467
|
self
|
|
472
468
|
end
|
|
473
469
|
|
|
@@ -590,7 +586,7 @@ module RobotLab
|
|
|
590
586
|
@backend[:session_id] = nil
|
|
591
587
|
@backend[:cache] = cached # Restore cache instance
|
|
592
588
|
end
|
|
593
|
-
@
|
|
589
|
+
@data = nil
|
|
594
590
|
self
|
|
595
591
|
end
|
|
596
592
|
|
|
@@ -641,7 +637,7 @@ module RobotLab
|
|
|
641
637
|
results: results.map(&:export),
|
|
642
638
|
messages: messages.map(&:to_h),
|
|
643
639
|
session_id: session_id,
|
|
644
|
-
custom: keys.
|
|
640
|
+
custom: keys.to_h { |k| [k, self[k]] }
|
|
645
641
|
}.compact
|
|
646
642
|
end
|
|
647
643
|
|
|
@@ -650,8 +646,8 @@ module RobotLab
|
|
|
650
646
|
# @param args [Array] arguments passed to to_json
|
|
651
647
|
# @return [String]
|
|
652
648
|
#
|
|
653
|
-
def to_json(*
|
|
654
|
-
to_h.to_json(*
|
|
649
|
+
def to_json(*)
|
|
650
|
+
to_h.to_json(*)
|
|
655
651
|
end
|
|
656
652
|
|
|
657
653
|
# Reconstruct memory from hash
|
|
@@ -687,7 +683,12 @@ module RobotLab
|
|
|
687
683
|
private
|
|
688
684
|
|
|
689
685
|
def document_store
|
|
690
|
-
|
|
686
|
+
unless RobotLab.extension_loaded?(:document_store)
|
|
687
|
+
raise RobotLab::DependencyError,
|
|
688
|
+
"document storage requires the robot_lab-document_store gem. " \
|
|
689
|
+
"Add `gem 'robot_lab-document_store'` to your Gemfile."
|
|
690
|
+
end
|
|
691
|
+
@document_store ||= RobotLab::DocumentStore.new
|
|
691
692
|
end
|
|
692
693
|
|
|
693
694
|
def create_semantic_cache
|
|
@@ -696,11 +697,9 @@ module RobotLab
|
|
|
696
697
|
|
|
697
698
|
def select_backend(preference)
|
|
698
699
|
case preference
|
|
699
|
-
when :redis
|
|
700
|
-
create_redis_backend || create_hash_backend
|
|
701
700
|
when :hash
|
|
702
701
|
create_hash_backend
|
|
703
|
-
else # :auto
|
|
702
|
+
else # :redis, :auto
|
|
704
703
|
create_redis_backend || create_hash_backend
|
|
705
704
|
end
|
|
706
705
|
end
|
|
@@ -722,7 +721,7 @@ module RobotLab
|
|
|
722
721
|
|
|
723
722
|
# Check if Redis is configured in RobotLab
|
|
724
723
|
redis_config = RobotLab.config.respond_to?(:redis) ? RobotLab.config.redis : nil
|
|
725
|
-
redis_config || ENV
|
|
724
|
+
redis_config || ENV.fetch("REDIS_URL", nil)
|
|
726
725
|
end
|
|
727
726
|
|
|
728
727
|
def get_internal(key)
|
|
@@ -877,8 +876,15 @@ module RobotLab
|
|
|
877
876
|
end
|
|
878
877
|
ensure
|
|
879
878
|
reschedule = @notification_queue_mutex.synchronize do
|
|
880
|
-
@
|
|
881
|
-
|
|
879
|
+
if @notification_queue.empty?
|
|
880
|
+
@drainer_scheduled = false
|
|
881
|
+
false
|
|
882
|
+
else
|
|
883
|
+
# Keep the flag set so concurrent writers don't also spawn a drainer.
|
|
884
|
+
# Only we will reschedule — no double-schedule race.
|
|
885
|
+
@drainer_scheduled = true
|
|
886
|
+
true
|
|
887
|
+
end
|
|
882
888
|
end
|
|
883
889
|
dispatch_async { drain_notification_queue } if reschedule
|
|
884
890
|
end
|
|
@@ -890,9 +896,9 @@ module RobotLab
|
|
|
890
896
|
def pattern_to_regex(pattern)
|
|
891
897
|
# Convert glob pattern to regex
|
|
892
898
|
regex_str = pattern
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
899
|
+
.gsub(".", "\\.")
|
|
900
|
+
.gsub("*", ".*")
|
|
901
|
+
.gsub("?", ".")
|
|
896
902
|
|
|
897
903
|
Regexp.new("\\A#{regex_str}\\z")
|
|
898
904
|
end
|
|
@@ -917,7 +923,6 @@ module RobotLab
|
|
|
917
923
|
def []=(key, value)
|
|
918
924
|
serialized = value.is_a?(String) ? value : value.to_json
|
|
919
925
|
@redis.set("#{@namespace}:#{key}", serialized)
|
|
920
|
-
value
|
|
921
926
|
end
|
|
922
927
|
|
|
923
928
|
def key?(key)
|
data/lib/robot_lab/message.rb
CHANGED
|
@@ -79,8 +79,8 @@ module RobotLab
|
|
|
79
79
|
#
|
|
80
80
|
# @param args [Array] arguments passed to to_json
|
|
81
81
|
# @return [String] JSON representation of the message
|
|
82
|
-
def to_json(*
|
|
83
|
-
to_h.to_json(*
|
|
82
|
+
def to_json(*)
|
|
83
|
+
to_h.to_json(*)
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
# Creates a Message instance from a hash.
|
|
@@ -200,8 +200,8 @@ module RobotLab
|
|
|
200
200
|
#
|
|
201
201
|
# @param args [Array] arguments passed to to_json
|
|
202
202
|
# @return [String] JSON representation
|
|
203
|
-
def to_json(*
|
|
204
|
-
to_h.to_json(*
|
|
203
|
+
def to_json(*)
|
|
204
|
+
to_h.to_json(*)
|
|
205
205
|
end
|
|
206
206
|
|
|
207
207
|
# Creates a ToolMessage from a hash.
|
data/lib/robot_lab/network.rb
CHANGED
|
@@ -86,7 +86,7 @@ module RobotLab
|
|
|
86
86
|
# task :billing, billing_robot, context: { dept: "billing" }, depends_on: :optional
|
|
87
87
|
# end
|
|
88
88
|
#
|
|
89
|
-
def initialize(name:, concurrency: :auto, memory: nil, config: nil, parallel_mode: :async, &
|
|
89
|
+
def initialize(name:, concurrency: :auto, memory: nil, config: nil, parallel_mode: :async, &)
|
|
90
90
|
@name = name.to_s
|
|
91
91
|
@robots = {}
|
|
92
92
|
@tasks = {}
|
|
@@ -97,7 +97,7 @@ module RobotLab
|
|
|
97
97
|
@broadcast_handlers = []
|
|
98
98
|
@bus_poller = BusPoller.new.start
|
|
99
99
|
|
|
100
|
-
instance_eval(&
|
|
100
|
+
instance_eval(&) if block_given?
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
# Add a robot as a pipeline task with optional per-task configuration
|
|
@@ -123,7 +123,8 @@ module RobotLab
|
|
|
123
123
|
# @example Task with dependencies
|
|
124
124
|
# task :writer, writer_robot, depends_on: [:analyst]
|
|
125
125
|
#
|
|
126
|
-
def task(name, robot, context: {}, mcp: :none, tools: :none, memory: nil, config: nil, depends_on: :none,
|
|
126
|
+
def task(name, robot, context: {}, mcp: :none, tools: :none, memory: nil, config: nil, depends_on: :none,
|
|
127
|
+
poller_group: :default)
|
|
127
128
|
task_wrapper = Task.new(
|
|
128
129
|
name: name,
|
|
129
130
|
robot: robot,
|
|
@@ -158,8 +159,8 @@ module RobotLab
|
|
|
158
159
|
# end
|
|
159
160
|
# task :process, processor, depends_on: :fetch_data
|
|
160
161
|
#
|
|
161
|
-
def parallel(name = nil, depends_on: :none, &
|
|
162
|
-
@pipeline.parallel(name, depends_on: depends_on, &
|
|
162
|
+
def parallel(name = nil, depends_on: :none, &)
|
|
163
|
+
@pipeline.parallel(name, depends_on: depends_on, &)
|
|
163
164
|
self
|
|
164
165
|
end
|
|
165
166
|
|
|
@@ -351,6 +352,11 @@ module RobotLab
|
|
|
351
352
|
private
|
|
352
353
|
|
|
353
354
|
def run_with_ractor_scheduler(run_context)
|
|
355
|
+
unless RobotLab.extension_loaded?(:ractor)
|
|
356
|
+
raise RobotLab::DependencyError,
|
|
357
|
+
"parallel_mode: :ractor requires the robot_lab-ractor gem. " \
|
|
358
|
+
"Add `gem 'robot_lab-ractor'` to your Gemfile."
|
|
359
|
+
end
|
|
354
360
|
message = run_context[:message].to_s
|
|
355
361
|
dep_graph = @pipeline.step_dependencies # { task_sym => [dep_sym, ...] }
|
|
356
362
|
|
|
@@ -373,6 +379,5 @@ module RobotLab
|
|
|
373
379
|
scheduler.shutdown
|
|
374
380
|
results
|
|
375
381
|
end
|
|
376
|
-
|
|
377
382
|
end
|
|
378
383
|
end
|