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
|
@@ -143,8 +143,8 @@ module RobotLab
|
|
|
143
143
|
#
|
|
144
144
|
# @param args [Array] arguments passed to to_json
|
|
145
145
|
# @return [String] JSON representation
|
|
146
|
-
def to_json(*
|
|
147
|
-
export.to_json(*
|
|
146
|
+
def to_json(*)
|
|
147
|
+
export.to_json(*)
|
|
148
148
|
end
|
|
149
149
|
|
|
150
150
|
# Get the last text content from output
|
|
@@ -152,9 +152,9 @@ module RobotLab
|
|
|
152
152
|
# @return [String, nil] The content of the last text message
|
|
153
153
|
#
|
|
154
154
|
def last_text_content
|
|
155
|
-
output.
|
|
155
|
+
output.rfind(&:text?)&.content
|
|
156
156
|
end
|
|
157
|
-
|
|
157
|
+
alias reply last_text_content
|
|
158
158
|
|
|
159
159
|
# Check if result contains tool calls
|
|
160
160
|
#
|
|
@@ -217,7 +217,8 @@ module RobotLab
|
|
|
217
217
|
when ToolResultMessage
|
|
218
218
|
result
|
|
219
219
|
when Hash
|
|
220
|
-
result[:type] == "tool_result" ? ToolResultMessage.new(**result.slice(:tool, :content,
|
|
220
|
+
result[:type] == "tool_result" ? ToolResultMessage.new(**result.slice(:tool, :content,
|
|
221
|
+
:stop_reason)) : Message.from_hash(result)
|
|
221
222
|
else
|
|
222
223
|
raise ArgumentError, "Invalid tool result: must be ToolResultMessage or Hash"
|
|
223
224
|
end
|
data/lib/robot_lab/run_config.rb
CHANGED
|
@@ -41,13 +41,15 @@ module RobotLab
|
|
|
41
41
|
CALLBACK_FIELDS = %i[on_tool_call on_tool_result on_content].freeze
|
|
42
42
|
|
|
43
43
|
# Infrastructure fields
|
|
44
|
-
INFRA_FIELDS = %i[bus enable_cache max_tool_rounds token_budget ractor_pool_size max_concurrent_robots
|
|
44
|
+
INFRA_FIELDS = %i[bus enable_cache max_tool_rounds token_budget ractor_pool_size max_concurrent_robots
|
|
45
|
+
doom_loop_threshold auto_compact compact_threshold].freeze
|
|
45
46
|
|
|
46
47
|
# All recognized fields
|
|
47
48
|
FIELDS = (LLM_FIELDS + TOOL_FIELDS + CALLBACK_FIELDS + INFRA_FIELDS).freeze
|
|
48
49
|
|
|
49
50
|
# Fields that cannot be serialized to JSON (Procs, IO objects, etc.)
|
|
50
|
-
|
|
51
|
+
# auto_compact is excluded because it may be a Proc.
|
|
52
|
+
NON_SERIALIZABLE_FIELDS = (CALLBACK_FIELDS + %i[bus auto_compact]).freeze
|
|
51
53
|
|
|
52
54
|
# Creates a new RunConfig.
|
|
53
55
|
#
|
|
@@ -85,15 +87,13 @@ module RobotLab
|
|
|
85
87
|
@fields.dup
|
|
86
88
|
end
|
|
87
89
|
|
|
88
|
-
|
|
89
90
|
# Returns a JSON-safe hash (skips Procs, IO, and other non-serializable values).
|
|
90
91
|
#
|
|
91
92
|
# @return [Hash]
|
|
92
93
|
def to_json_hash
|
|
93
|
-
@fields.
|
|
94
|
+
@fields.except(*NON_SERIALIZABLE_FIELDS)
|
|
94
95
|
end
|
|
95
96
|
|
|
96
|
-
|
|
97
97
|
# Merges another RunConfig (or Hash) on top of this one.
|
|
98
98
|
# The other's non-nil values win. Returns a new RunConfig.
|
|
99
99
|
#
|
|
@@ -105,7 +105,6 @@ module RobotLab
|
|
|
105
105
|
self.class.new(**merged)
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
-
|
|
109
108
|
# Applies LLM fields to a chat object via its with_* methods.
|
|
110
109
|
#
|
|
111
110
|
# @param chat [Object] a RubyLLM::Chat (or similar) that responds to with_model, with_temperature, etc.
|
|
@@ -119,7 +118,6 @@ module RobotLab
|
|
|
119
118
|
end
|
|
120
119
|
end
|
|
121
120
|
|
|
122
|
-
|
|
123
121
|
# Build a RunConfig from prompt_manager front matter metadata.
|
|
124
122
|
#
|
|
125
123
|
# @param metadata [Object] a PM::Metadata object (responds to field names)
|
|
@@ -141,27 +139,23 @@ module RobotLab
|
|
|
141
139
|
new(**fields)
|
|
142
140
|
end
|
|
143
141
|
|
|
144
|
-
|
|
145
142
|
# @return [Boolean] true if no fields have been set
|
|
146
143
|
def empty?
|
|
147
144
|
@fields.empty?
|
|
148
145
|
end
|
|
149
146
|
|
|
150
|
-
|
|
151
147
|
# @param field [Symbol] the field name
|
|
152
148
|
# @return [Boolean] true if the field has been explicitly set
|
|
153
149
|
def key?(field)
|
|
154
150
|
@fields.key?(field)
|
|
155
151
|
end
|
|
156
152
|
|
|
157
|
-
|
|
158
153
|
# @param other [RunConfig] the other RunConfig to compare
|
|
159
154
|
# @return [Boolean]
|
|
160
155
|
def ==(other)
|
|
161
156
|
other.is_a?(RunConfig) && to_h == other.to_h
|
|
162
157
|
end
|
|
163
158
|
|
|
164
|
-
|
|
165
159
|
# @return [String]
|
|
166
160
|
def inspect
|
|
167
161
|
"#<#{self.class} #{@fields.inspect}>"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'shellwords'
|
|
5
|
+
|
|
6
|
+
module RobotLab
|
|
7
|
+
# Factory module for wrapping AgentSkills scripts as RobotLab::Tool instances.
|
|
8
|
+
#
|
|
9
|
+
# Given a path to an executable shell script, produces a Tool that shells
|
|
10
|
+
# out to the script and returns its combined stdout+stderr output.
|
|
11
|
+
# Non-executable scripts return nil with a logged warning.
|
|
12
|
+
module ScriptTool
|
|
13
|
+
# Wrap a script file as a RobotLab::Tool.
|
|
14
|
+
#
|
|
15
|
+
# @param script_path [String, Pathname] path to the script file
|
|
16
|
+
# @return [RobotLab::Tool, nil] nil if the script is not executable
|
|
17
|
+
def self.from_path(script_path)
|
|
18
|
+
path = Pathname.new(script_path)
|
|
19
|
+
|
|
20
|
+
unless path.executable?
|
|
21
|
+
RobotLab.config.logger.warn(
|
|
22
|
+
"ScriptTool: #{path.basename} is not executable, skipping"
|
|
23
|
+
)
|
|
24
|
+
return nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
tool_name = derive_name(path)
|
|
28
|
+
description = extract_description(path)
|
|
29
|
+
script = path.to_s
|
|
30
|
+
|
|
31
|
+
Tool.create(
|
|
32
|
+
name: tool_name,
|
|
33
|
+
description: description,
|
|
34
|
+
parameters: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
args: { type: 'string', description: 'Optional command-line arguments' }
|
|
38
|
+
},
|
|
39
|
+
required: []
|
|
40
|
+
}
|
|
41
|
+
) do |tool_args|
|
|
42
|
+
cli_args = tool_args[:args].to_s.strip
|
|
43
|
+
cmd = cli_args.empty? ? ['bash', script] : ['bash', script, *Shellwords.split(cli_args)]
|
|
44
|
+
output, status = Open3.capture2e(*cmd)
|
|
45
|
+
status.success? ? output : "Error (exit #{status.exitstatus}):\n#{output}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @param path [Pathname]
|
|
50
|
+
# @return [String] snake_case tool name derived from filename
|
|
51
|
+
def self.derive_name(path)
|
|
52
|
+
path.basename.to_s
|
|
53
|
+
.sub(/\.[^.]+$/, '')
|
|
54
|
+
.gsub(/[^a-zA-Z0-9]+/, '_')
|
|
55
|
+
.gsub(/^_+|_+$/, '')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Extract tool description from the first non-shebang comment line.
|
|
59
|
+
#
|
|
60
|
+
# @param path [Pathname]
|
|
61
|
+
# @return [String]
|
|
62
|
+
def self.extract_description(path)
|
|
63
|
+
File.foreach(path) do |line|
|
|
64
|
+
stripped = line.strip
|
|
65
|
+
next unless stripped.start_with?('#')
|
|
66
|
+
next if stripped.start_with?('#!') # skip shebang
|
|
67
|
+
|
|
68
|
+
desc = stripped.sub(/^#+\s*/, '').strip
|
|
69
|
+
return desc unless desc.empty?
|
|
70
|
+
end
|
|
71
|
+
derive_name(path)
|
|
72
|
+
rescue StandardError
|
|
73
|
+
derive_name(path)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -46,7 +46,6 @@ module RobotLab
|
|
|
46
46
|
old_value = @data[key]
|
|
47
47
|
@data[key] = value
|
|
48
48
|
@on_change&.call(key, old_value, value) if old_value != value
|
|
49
|
-
value
|
|
50
49
|
end
|
|
51
50
|
|
|
52
51
|
# Check if key exists
|
|
@@ -85,8 +84,12 @@ module RobotLab
|
|
|
85
84
|
#
|
|
86
85
|
# @yield [Symbol, Object]
|
|
87
86
|
#
|
|
88
|
-
def each(&
|
|
89
|
-
@data.each(&
|
|
87
|
+
def each(&)
|
|
88
|
+
@data.each(&)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def map(&)
|
|
92
|
+
@data.map(&)
|
|
90
93
|
end
|
|
91
94
|
|
|
92
95
|
# Delete a key
|
|
@@ -155,7 +158,7 @@ module RobotLab
|
|
|
155
158
|
# proxy.name # Same as proxy[:name]
|
|
156
159
|
# proxy.name = "x" # Same as proxy[:name] = "x"
|
|
157
160
|
#
|
|
158
|
-
def method_missing(method_name, *args, &
|
|
161
|
+
def method_missing(method_name, *args, &)
|
|
159
162
|
method_str = method_name.to_s
|
|
160
163
|
|
|
161
164
|
if method_str.end_with?("=")
|
|
@@ -173,6 +176,5 @@ module RobotLab
|
|
|
173
176
|
def inspect
|
|
174
177
|
"#<RobotLab::StateProxy #{@data.inspect}>"
|
|
175
178
|
end
|
|
176
|
-
|
|
177
179
|
end
|
|
178
180
|
end
|
data/lib/robot_lab/tool.rb
CHANGED
|
@@ -94,7 +94,7 @@ module RobotLab
|
|
|
94
94
|
# @param args [Hash] the tool arguments from the LLM
|
|
95
95
|
# @return [Object] the tool result or an error string
|
|
96
96
|
def call(args)
|
|
97
|
-
if self.class.ractor_safe? && !self.class.name.nil?
|
|
97
|
+
if self.class.ractor_safe? && !self.class.name.nil? && RobotLab.extension_loaded?(:ractor)
|
|
98
98
|
RobotLab.ractor_pool.submit(self.class.name, args)
|
|
99
99
|
else
|
|
100
100
|
super
|
|
@@ -204,8 +204,8 @@ module RobotLab
|
|
|
204
204
|
#
|
|
205
205
|
# @param args [Array] arguments passed to to_json
|
|
206
206
|
# @return [String]
|
|
207
|
-
def to_json(*
|
|
208
|
-
to_h.to_json(*
|
|
207
|
+
def to_json(*)
|
|
208
|
+
to_h.to_json(*)
|
|
209
209
|
end
|
|
210
210
|
|
|
211
211
|
private
|
|
@@ -94,7 +94,7 @@ module RobotLab
|
|
|
94
94
|
def filter_tools(tools, allowed_names:)
|
|
95
95
|
return [] if allowed_names.empty?
|
|
96
96
|
|
|
97
|
-
allowed_set = allowed_names.
|
|
97
|
+
allowed_set = allowed_names.to_set(&:to_s)
|
|
98
98
|
tools.select { |tool| allowed_set.include?(tool_name(tool)) }
|
|
99
99
|
end
|
|
100
100
|
|
|
@@ -142,8 +142,8 @@ module RobotLab
|
|
|
142
142
|
#
|
|
143
143
|
# @yield [Tool] Each tool in the manifest
|
|
144
144
|
#
|
|
145
|
-
def each(&
|
|
146
|
-
@tools.values.each(&
|
|
145
|
+
def each(&)
|
|
146
|
+
@tools.values.each(&)
|
|
147
147
|
end
|
|
148
148
|
|
|
149
149
|
# Clear all tools
|
|
@@ -173,9 +173,7 @@ module RobotLab
|
|
|
173
173
|
#
|
|
174
174
|
def merge(other)
|
|
175
175
|
case other
|
|
176
|
-
when ToolManifest
|
|
177
|
-
other.each { |tool| add(tool) }
|
|
178
|
-
when Array
|
|
176
|
+
when ToolManifest, Array
|
|
179
177
|
other.each { |tool| add(tool) }
|
|
180
178
|
when Tool
|
|
181
179
|
add(other)
|
|
@@ -194,8 +192,8 @@ module RobotLab
|
|
|
194
192
|
#
|
|
195
193
|
# @param args [Array] arguments passed to to_json
|
|
196
194
|
# @return [String] JSON representation
|
|
197
|
-
def to_json(*
|
|
198
|
-
to_h.to_json(*
|
|
195
|
+
def to_json(*)
|
|
196
|
+
to_h.to_json(*)
|
|
199
197
|
end
|
|
200
198
|
|
|
201
199
|
# Create manifest from hash of tool definitions
|
data/lib/robot_lab/version.rb
CHANGED
data/lib/robot_lab/waiter.rb
CHANGED
data/lib/robot_lab.rb
CHANGED
|
@@ -6,9 +6,6 @@ require 'securerandom'
|
|
|
6
6
|
require 'digest'
|
|
7
7
|
|
|
8
8
|
# Core dependencies
|
|
9
|
-
# ActiveSupport delegation is required by ruby_llm (RubyLLM::Agent uses delegate)
|
|
10
|
-
# but not declared in ruby_llm's gemspec. Load it before ruby_llm.
|
|
11
|
-
require 'active_support/core_ext/module/delegation'
|
|
12
9
|
require 'ruby_llm'
|
|
13
10
|
require 'prompt_manager'
|
|
14
11
|
require 'async'
|
|
@@ -49,8 +46,6 @@ module RobotLab
|
|
|
49
46
|
end
|
|
50
47
|
|
|
51
48
|
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
|
52
|
-
loader.ignore("#{__dir__}/generators")
|
|
53
|
-
loader.ignore("#{__dir__}/robot_lab/rails_integration")
|
|
54
49
|
loader.ignore("#{__dir__}/robot_lab/robot")
|
|
55
50
|
|
|
56
51
|
# Custom inflections for classes that don't follow Zeitwerk naming conventions
|
|
@@ -71,7 +66,6 @@ loader.setup
|
|
|
71
66
|
require_relative 'robot_lab/error'
|
|
72
67
|
require_relative 'robot_lab/message'
|
|
73
68
|
require_relative 'robot_lab/memory'
|
|
74
|
-
require_relative 'robot_lab/ractor_job'
|
|
75
69
|
|
|
76
70
|
# Eager load everything in Rails or when explicitly requested.
|
|
77
71
|
# Otherwise Zeitwerk's lazy autoloading keeps boot fast.
|
|
@@ -80,7 +74,41 @@ loader.eager_load if defined?(Rails::Engine) || ENV["ROBOT_LAB_EAGER_LOAD"]
|
|
|
80
74
|
module RobotLab
|
|
81
75
|
# Error classes are defined in lib/robot_lab/error.rb
|
|
82
76
|
|
|
77
|
+
@extensions = {}
|
|
78
|
+
|
|
83
79
|
class << self
|
|
80
|
+
# Registers an extension gem so core can detect it without depending on it.
|
|
81
|
+
#
|
|
82
|
+
# Extension gems call this at load time to announce themselves. Core uses
|
|
83
|
+
# extension_loaded? to guard optional behavior instead of defined?/respond_to?.
|
|
84
|
+
#
|
|
85
|
+
# @param name [Symbol] identifier for the extension (e.g. :durable, :ractor)
|
|
86
|
+
# @param mod [Module, Object] the extension's primary module or a sentinel value
|
|
87
|
+
def register_extension(name, mod)
|
|
88
|
+
@extensions[name] = mod
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns true if the named extension gem has been loaded.
|
|
92
|
+
#
|
|
93
|
+
# @param name [Symbol] extension identifier
|
|
94
|
+
def extension_loaded?(name)
|
|
95
|
+
@extensions.key?(name)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Returns the module registered for the named extension, or nil.
|
|
99
|
+
#
|
|
100
|
+
# @param name [Symbol] extension identifier
|
|
101
|
+
def extension(name)
|
|
102
|
+
@extensions[name]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns the list of registered extension names.
|
|
106
|
+
#
|
|
107
|
+
# @return [Array<Symbol>]
|
|
108
|
+
def loaded_extensions
|
|
109
|
+
@extensions.keys
|
|
110
|
+
end
|
|
111
|
+
|
|
84
112
|
# Returns the Config object (MywayConfig-based).
|
|
85
113
|
#
|
|
86
114
|
# Configuration is automatically loaded from:
|
|
@@ -100,7 +128,6 @@ module RobotLab
|
|
|
100
128
|
@config ||= Config.new.tap(&:after_load)
|
|
101
129
|
end
|
|
102
130
|
|
|
103
|
-
|
|
104
131
|
# Yields the Config object for block-style configuration.
|
|
105
132
|
#
|
|
106
133
|
# @yield [Config] the config instance
|
|
@@ -114,7 +141,6 @@ module RobotLab
|
|
|
114
141
|
yield config
|
|
115
142
|
end
|
|
116
143
|
|
|
117
|
-
|
|
118
144
|
# Reload configuration from all sources.
|
|
119
145
|
#
|
|
120
146
|
# Clears the cached Config instance, forcing it to be
|
|
@@ -126,7 +152,6 @@ module RobotLab
|
|
|
126
152
|
config
|
|
127
153
|
end
|
|
128
154
|
|
|
129
|
-
|
|
130
155
|
# Factory method to create a new Robot instance.
|
|
131
156
|
#
|
|
132
157
|
# @param name [String, nil] the unique identifier for the robot (auto-generated if nil)
|
|
@@ -153,7 +178,8 @@ module RobotLab
|
|
|
153
178
|
# name: "helper",
|
|
154
179
|
# system_prompt: "You are a helpful assistant."
|
|
155
180
|
# )
|
|
156
|
-
def build(name: "robot", template: nil, system_prompt: nil, context: {}, enable_cache: true, bus: nil, skills: nil,
|
|
181
|
+
def build(name: "robot", template: nil, system_prompt: nil, context: {}, enable_cache: true, bus: nil, skills: nil,
|
|
182
|
+
config: nil, **)
|
|
157
183
|
Robot.new(
|
|
158
184
|
name: name,
|
|
159
185
|
template: template,
|
|
@@ -163,11 +189,10 @@ module RobotLab
|
|
|
163
189
|
bus: bus,
|
|
164
190
|
skills: skills,
|
|
165
191
|
config: config,
|
|
166
|
-
**
|
|
192
|
+
**
|
|
167
193
|
)
|
|
168
194
|
end
|
|
169
195
|
|
|
170
|
-
|
|
171
196
|
# Factory method to create a new Network of robots.
|
|
172
197
|
#
|
|
173
198
|
# @param name [String] the unique identifier for the network
|
|
@@ -195,11 +220,10 @@ module RobotLab
|
|
|
195
220
|
# step :entities, entity_bot, depends_on: [:fetch]
|
|
196
221
|
# step :merge, merger, depends_on: [:sentiment, :entities]
|
|
197
222
|
# end
|
|
198
|
-
def create_network(name:, concurrency: :auto, config: nil, &
|
|
199
|
-
Network.new(name: name, concurrency: concurrency, config: config, &
|
|
223
|
+
def create_network(name:, concurrency: :auto, config: nil, &)
|
|
224
|
+
Network.new(name: name, concurrency: concurrency, config: config, &)
|
|
200
225
|
end
|
|
201
226
|
|
|
202
|
-
|
|
203
227
|
# Factory method to create a new Memory object.
|
|
204
228
|
#
|
|
205
229
|
# @param data [Hash] initial runtime data
|
|
@@ -216,43 +240,8 @@ module RobotLab
|
|
|
216
240
|
#
|
|
217
241
|
# @example Memory with caching disabled
|
|
218
242
|
# memory = RobotLab.create_memory(data: {}, enable_cache: false)
|
|
219
|
-
def create_memory(data: {}, enable_cache: true, **
|
|
220
|
-
Memory.new(data: data, enable_cache: enable_cache, **
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
# Returns the shared RactorWorkerPool, lazily initialized.
|
|
225
|
-
#
|
|
226
|
-
# Pool size is determined by RobotLab.config.ractor_pool_size or
|
|
227
|
-
# defaults to Etc.nprocessors (:auto). The pool lives for the lifetime
|
|
228
|
-
# of the process. Call RobotLab.shutdown_ractor_pool to drain and
|
|
229
|
-
# close it explicitly.
|
|
230
|
-
#
|
|
231
|
-
# @return [RactorWorkerPool]
|
|
232
|
-
def ractor_pool
|
|
233
|
-
@ractor_pool ||= begin
|
|
234
|
-
size = config.respond_to?(:ractor_pool_size) ? (config.ractor_pool_size || :auto) : :auto
|
|
235
|
-
RactorWorkerPool.new(size: size)
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
# Shut down the shared Ractor worker pool, draining in-flight jobs.
|
|
240
|
-
#
|
|
241
|
-
# @return [void]
|
|
242
|
-
def shutdown_ractor_pool
|
|
243
|
-
@ractor_pool&.shutdown
|
|
244
|
-
@ractor_pool = nil
|
|
243
|
+
def create_memory(data: {}, enable_cache: true, **)
|
|
244
|
+
Memory.new(data: data, enable_cache: enable_cache, **)
|
|
245
245
|
end
|
|
246
246
|
end
|
|
247
247
|
end
|
|
248
|
-
|
|
249
|
-
# Load Rails integration if Rails is defined
|
|
250
|
-
if defined?(Rails::Engine)
|
|
251
|
-
require 'robot_lab/rails_integration/engine'
|
|
252
|
-
require 'robot_lab/rails_integration/railtie'
|
|
253
|
-
require 'robot_lab/rails_integration/turbo_stream_callbacks'
|
|
254
|
-
require 'robot_lab/rails_integration/job'
|
|
255
|
-
|
|
256
|
-
# Convenience alias so job subclasses can inherit from RobotLab::Job
|
|
257
|
-
RobotLab::Job = RobotLab::RailsIntegration::Job
|
|
258
|
-
end
|
data/logfile
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
2026-02-25 20:56:30.644 CST [47152] LOG: starting PostgreSQL 18.2 (Homebrew) on aarch64-apple-darwin25.2.0, compiled by Apple clang version 17.0.0 (clang-1700.6.3.2), 64-bit
|
|
2
|
+
2026-02-25 20:56:30.646 CST [47152] LOG: could not bind IPv6 address "::1": Address already in use
|
|
3
|
+
2026-02-25 20:56:30.646 CST [47152] HINT: Is another postmaster already running on port 5432? If not, wait a few seconds and retry.
|
|
4
|
+
2026-02-25 20:56:30.646 CST [47152] LOG: could not bind IPv4 address "127.0.0.1": Address already in use
|
|
5
|
+
2026-02-25 20:56:30.646 CST [47152] HINT: Is another postmaster already running on port 5432? If not, wait a few seconds and retry.
|
|
6
|
+
2026-02-25 20:56:30.646 CST [47152] WARNING: could not create listen socket for "localhost"
|
|
7
|
+
2026-02-25 20:56:30.646 CST [47152] FATAL: could not create any TCP/IP sockets
|
|
8
|
+
2026-02-25 20:56:30.646 CST [47152] LOG: database system is shut down
|
data/mkdocs.yml
CHANGED
|
@@ -169,8 +169,8 @@ nav:
|
|
|
169
169
|
- MCP Integration: guides/mcp-integration.md
|
|
170
170
|
- Streaming Responses: guides/streaming.md
|
|
171
171
|
- Memory System: guides/memory.md
|
|
172
|
-
-
|
|
173
|
-
-
|
|
172
|
+
- Observability & Safety: guides/observability.md
|
|
173
|
+
- Knowledge Search: guides/knowledge.md
|
|
174
174
|
- API Reference:
|
|
175
175
|
- api/index.md
|
|
176
176
|
- Core Classes:
|
|
@@ -201,4 +201,3 @@ nav:
|
|
|
201
201
|
- Multi-Robot Network: examples/multi-robot-network.md
|
|
202
202
|
- Tool Usage: examples/tool-usage.md
|
|
203
203
|
- MCP Server: examples/mcp-server.md
|
|
204
|
-
- Rails Application: examples/rails-application.md
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Ruby Concurrency Review — RobotLab
|
|
2
|
+
|
|
3
|
+
Source: https://paolino.me/ruby-concurrency-what-actually-happens/
|
|
4
|
+
|
|
5
|
+
## What Applies to RobotLab
|
|
6
|
+
|
|
7
|
+
### 1. You're Already in the Right Model — Lean Into It
|
|
8
|
+
|
|
9
|
+
RobotLab uses `async (~> 2.0)`, which is exactly the fiber-based scheduler the article describes. LLM calls are pure I/O-bound work (HTTP streaming), so fibers are the correct and lowest-cost primitive. The article validates this architecture choice. No changes needed here — but be deliberate about not mixing blocking thread-style calls into the `Async` reactor unnecessarily.
|
|
10
|
+
|
|
11
|
+
### 2. The Ractor Caution Is Real (Recent Commit: `feat(ractor-parallelism)`)
|
|
12
|
+
|
|
13
|
+
The article is clear: Ractors in Ruby 4.0 are still experimental and practically incompatible with gems that use global state. RobotLab's dependencies (`ruby_llm`, `prompt_manager`, `zeitwerk`) almost certainly use global state. Worth auditing whether the new Ractor code hits `Ractor::IsolationError` under realistic load. The article's recommendation: **use Processes** for CPU parallelism if Rails-style gems are involved.
|
|
14
|
+
|
|
15
|
+
### 3. ~~The `Waiter` Class May Be Misfit in Fiber Context~~ — RESOLVED
|
|
16
|
+
|
|
17
|
+
`lib/robot_lab/waiter.rb` was updated to use `IO.pipe` + `IO.select` instead of `ConditionVariable`. `IO.select` is hooked by the Ruby 3.1+ fiber scheduler protocol, so it correctly yields to the Async scheduler when called from within an Async task, and correctly blocks the calling thread when used outside one. `Async::Condition` was considered but rejected: it only works inside an Async block, whereas `Memory` is intentionally usable from both plain threads and Async fibers.
|
|
18
|
+
|
|
19
|
+
### 4. Parallel Network Execution — Check the Primitive
|
|
20
|
+
|
|
21
|
+
`call_parallel()` in `Network` (built on `SimpleFlow::Pipeline`) — if it spawns OS threads rather than async tasks, you get heavier overhead than necessary for I/O-bound robot calls. The article's guidance: for I/O work, fiber-based `Async::Barrier` with concurrent tasks is 10-20x cheaper than threads.
|
|
22
|
+
|
|
23
|
+
### 5. Resource Semaphores for Parallel Networks
|
|
24
|
+
|
|
25
|
+
When a Network runs multiple robots concurrently, each making LLM API calls, you can exhaust API rate limits or connection pools with no back-pressure. The article's semaphore pattern applies directly — cap in-flight robot tasks to match your API tier's concurrency limit. This is worth adding to `RunConfig` as a `max_concurrent_robots` infra field.
|
|
26
|
+
|
|
27
|
+
### 6. Colorless Concurrency — This Is Already Your Story
|
|
28
|
+
|
|
29
|
+
The article's main point about Ruby fibers being "colorless" (no `async/await` propagation required) is directly reflected in how `robot.run("...")` stays synchronous-looking whether run standalone or in a parallel network. This is an underappreciated design strength worth highlighting in docs/README.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Summary
|
|
34
|
+
|
|
35
|
+
The async/fiber architecture is correct for LLM I/O work. The two actionable concerns are:
|
|
36
|
+
|
|
37
|
+
1. **Audit the Ractor code** for isolation errors with gem dependencies (`ruby_llm`, `prompt_manager`, `zeitwerk`).
|
|
38
|
+
2. **Check `Waiter`** (condition variable in `lib/robot_lab/waiter.rb`) — if called inside an `Async` reactor block, replace with `Async::Condition` to avoid stalling the reactor.
|