robot_lab 0.1.0 → 0.2.6
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/.envrc +1 -0
- data/.irbrc +2 -2
- data/.loki +60 -0
- data/.quality/reek_baseline.txt +43 -0
- data/.rubocop.yml +10 -0
- data/CHANGELOG.md +126 -0
- data/CLAUDE.md +139 -0
- data/README.md +130 -73
- data/Rakefile +131 -9
- data/agent2agent_review.md +192 -0
- data/agentf_improvements.md +253 -0
- data/agents.md +14 -0
- data/docs/api/core/index.md +1 -0
- data/docs/api/core/network.md +31 -1
- data/docs/api/core/robot.md +91 -5
- data/docs/api/errors.md +47 -2
- data/docs/api/index.md +3 -0
- data/docs/architecture/core-concepts.md +40 -0
- data/docs/architecture/network-orchestration.md +8 -0
- data/docs/architecture/robot-execution.md +1 -1
- data/docs/examples/index.md +37 -2
- data/docs/getting-started/configuration.md +39 -7
- data/docs/guides/building-robots.md +39 -1
- data/docs/guides/creating-networks.md +6 -1
- data/docs/guides/hooks.md +997 -0
- data/docs/guides/index.md +19 -14
- data/docs/guides/knowledge.md +9 -3
- data/docs/guides/observability.md +206 -7
- data/docs/guides/using-tools.md +69 -0
- data/docs/index.md +31 -4
- 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 +18 -10
- 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/35_hooks.rb +256 -0
- data/examples/README.md +8 -1
- data/examples/common.rb +80 -0
- data/examples/ruboruby.md +423 -0
- data/examples/xyzzy.rb +90 -0
- data/lib/robot_lab/agent_skill.rb +64 -0
- data/lib/robot_lab/agent_skill_catalog.rb +74 -0
- data/lib/robot_lab/ask_user.rb +2 -2
- data/lib/robot_lab/budget/ledger.rb +98 -0
- data/lib/robot_lab/bus_poller.rb +12 -5
- data/lib/robot_lab/capabilities.rb +84 -0
- data/lib/robot_lab/config/defaults.yml +10 -0
- data/lib/robot_lab/config.rb +5 -16
- data/lib/robot_lab/delegation_future.rb +1 -1
- data/lib/robot_lab/doom_loop_detector.rb +98 -0
- data/lib/robot_lab/error.rb +34 -4
- data/lib/robot_lab/errors.rb +45 -0
- data/lib/robot_lab/history_compressor.rb +4 -10
- data/lib/robot_lab/hook.rb +79 -0
- data/lib/robot_lab/hook_context.rb +194 -0
- data/lib/robot_lab/hook_registry.rb +55 -0
- data/lib/robot_lab/hooks.rb +87 -0
- data/lib/robot_lab/mcp/client.rb +1 -2
- data/lib/robot_lab/mcp/connection_poller.rb +5 -5
- 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/narrator.rb +87 -0
- data/lib/robot_lab/network.rb +87 -25
- data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
- data/lib/robot_lab/robot/budget.rb +89 -0
- data/lib/robot_lab/robot/bus_messaging.rb +79 -33
- data/lib/robot_lab/robot/history_search.rb +4 -1
- data/lib/robot_lab/robot/hooking.rb +56 -0
- data/lib/robot_lab/robot/mcp_management.rb +12 -12
- data/lib/robot_lab/robot/template_rendering.rb +62 -42
- data/lib/robot_lab/robot.rb +482 -242
- data/lib/robot_lab/robot_result.rb +6 -5
- data/lib/robot_lab/run_config.rb +27 -14
- data/lib/robot_lab/runnable.rb +51 -0
- data/lib/robot_lab/sandbox/null.rb +13 -0
- data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
- data/lib/robot_lab/sandbox.rb +52 -0
- data/lib/robot_lab/script_tool.rb +134 -0
- data/lib/robot_lab/state_proxy.rb +7 -5
- data/lib/robot_lab/task.rb +26 -20
- data/lib/robot_lab/tool.rb +54 -13
- 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 +89 -51
- data/logfile +8 -0
- data/mkdocs.yml +3 -3
- data/robot_concurrency.md +38 -0
- data/simple_acp_review.md +298 -0
- data/temp.md +6 -0
- data/tool_manifest_plan.md +155 -0
- metadata +82 -83
- 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/tool.rb
CHANGED
|
@@ -94,19 +94,17 @@ 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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
super
|
|
97
|
+
context = ToolCallHookContext.new(tool: self, tool_args: args, robot: @robot)
|
|
98
|
+
|
|
99
|
+
RobotLab::Hooks.run(:tool_call, context, **tool_hook_options) do
|
|
100
|
+
context.tool_result = use_ractor_pool? ? execute_ractor_tool_call(args) : super
|
|
101
|
+
rescue RobotLab::ToolError => e
|
|
102
|
+
handle_tool_error(context, e)
|
|
103
|
+
rescue StandardError => e
|
|
104
|
+
handle_standard_error(context, e)
|
|
101
105
|
end
|
|
102
|
-
rescue RobotLab::ToolError => e
|
|
103
|
-
raise if self.class.raise_on_error?
|
|
104
|
-
"Error (#{name}): #{e.message}"
|
|
105
|
-
rescue StandardError => e
|
|
106
|
-
raise if self.class.raise_on_error?
|
|
107
106
|
|
|
108
|
-
|
|
109
|
-
"Error (#{name}): #{e.message}"
|
|
107
|
+
context.tool_result
|
|
110
108
|
end
|
|
111
109
|
|
|
112
110
|
# Override name to support explicit names for dynamic/MCP tools.
|
|
@@ -204,12 +202,55 @@ module RobotLab
|
|
|
204
202
|
#
|
|
205
203
|
# @param args [Array] arguments passed to to_json
|
|
206
204
|
# @return [String]
|
|
207
|
-
def to_json(*
|
|
208
|
-
to_h.to_json(*
|
|
205
|
+
def to_json(*)
|
|
206
|
+
to_h.to_json(*)
|
|
209
207
|
end
|
|
210
208
|
|
|
211
209
|
private
|
|
212
210
|
|
|
211
|
+
def tool_hook_options
|
|
212
|
+
hook_scope = RobotLab.current_hook_scope
|
|
213
|
+
|
|
214
|
+
{
|
|
215
|
+
registries: tool_hook_registries(hook_scope),
|
|
216
|
+
per_run_hooks: hook_scope && hook_scope[:per_run_hooks]
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def tool_hook_registries(hook_scope)
|
|
221
|
+
return hook_scope[:registries] if hook_scope
|
|
222
|
+
|
|
223
|
+
[RobotLab.hooks, robot_hook_registry]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def robot_hook_registry
|
|
227
|
+
@robot.hooks if @robot.respond_to?(:hooks)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def execute_ractor_tool_call(args)
|
|
231
|
+
RobotLab.ractor_pool.submit(self.class.name, args)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def use_ractor_pool?
|
|
235
|
+
self.class.ractor_safe? && !self.class.name.nil? && RobotLab.extension_loaded?(:ractor)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def handle_tool_error(context, error)
|
|
239
|
+
context.tool_error = error
|
|
240
|
+
raise if self.class.raise_on_error?
|
|
241
|
+
|
|
242
|
+
suffix = RobotLab::Errors.retryable?(error) ? " (retryable)" : ""
|
|
243
|
+
context.tool_result = "Error (#{name}): #{error.message}#{suffix}"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def handle_standard_error(context, error)
|
|
247
|
+
context.tool_error = error
|
|
248
|
+
raise if self.class.raise_on_error?
|
|
249
|
+
|
|
250
|
+
RobotLab.config.logger.warn("Tool '#{name}' error: #{error.class}: #{error.message}")
|
|
251
|
+
context.tool_result = "Error (#{name}): #{error.message}"
|
|
252
|
+
end
|
|
253
|
+
|
|
213
254
|
def deep_symbolize_keys(obj)
|
|
214
255
|
case obj
|
|
215
256
|
when Hash
|
|
@@ -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
|
|
@@ -69,9 +64,12 @@ loader.setup
|
|
|
69
64
|
# individually (e.g. TextMessage lives in message.rb, not text_message.rb).
|
|
70
65
|
# Require them explicitly so their constants are available without eager loading.
|
|
71
66
|
require_relative 'robot_lab/error'
|
|
67
|
+
require_relative 'robot_lab/hook_context'
|
|
68
|
+
require_relative 'robot_lab/hook'
|
|
69
|
+
require_relative 'robot_lab/hook_registry'
|
|
70
|
+
require_relative 'robot_lab/hooks'
|
|
72
71
|
require_relative 'robot_lab/message'
|
|
73
72
|
require_relative 'robot_lab/memory'
|
|
74
|
-
require_relative 'robot_lab/ractor_job'
|
|
75
73
|
|
|
76
74
|
# Eager load everything in Rails or when explicitly requested.
|
|
77
75
|
# Otherwise Zeitwerk's lazy autoloading keeps boot fast.
|
|
@@ -80,7 +78,68 @@ loader.eager_load if defined?(Rails::Engine) || ENV["ROBOT_LAB_EAGER_LOAD"]
|
|
|
80
78
|
module RobotLab
|
|
81
79
|
# Error classes are defined in lib/robot_lab/error.rb
|
|
82
80
|
|
|
81
|
+
@extensions = {}
|
|
82
|
+
|
|
83
83
|
class << self
|
|
84
|
+
# Registers an extension gem so core can detect it without depending on it.
|
|
85
|
+
#
|
|
86
|
+
# Extension gems call this at load time to announce themselves. Core uses
|
|
87
|
+
# extension_loaded? to guard optional behavior instead of defined?/respond_to?.
|
|
88
|
+
#
|
|
89
|
+
# @param name [Symbol] identifier for the extension (e.g. :durable, :ractor)
|
|
90
|
+
# @param mod [Module, Object] the extension's primary module or a sentinel value
|
|
91
|
+
def register_extension(name, mod)
|
|
92
|
+
@extensions[name] = mod
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Returns true if the named extension gem has been loaded.
|
|
96
|
+
#
|
|
97
|
+
# @param name [Symbol] extension identifier
|
|
98
|
+
def extension_loaded?(name)
|
|
99
|
+
@extensions.key?(name)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns the module registered for the named extension, or nil.
|
|
103
|
+
#
|
|
104
|
+
# @param name [Symbol] extension identifier
|
|
105
|
+
def extension(name)
|
|
106
|
+
@extensions[name]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Returns the list of registered extension names.
|
|
110
|
+
#
|
|
111
|
+
# @return [Array<Symbol>]
|
|
112
|
+
def loaded_extensions
|
|
113
|
+
@extensions.keys
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def hooks
|
|
117
|
+
@hooks ||= HookRegistry.new
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def on(handler_class, context: nil)
|
|
121
|
+
hooks.on(handler_class, context: context)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def clear_hooks!
|
|
125
|
+
@hooks = HookRegistry.new
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def with_hook_scope(registries, per_run_hooks)
|
|
129
|
+
previous = Thread.current[:robot_lab_hook_scope]
|
|
130
|
+
Thread.current[:robot_lab_hook_scope] = {
|
|
131
|
+
registries: registries,
|
|
132
|
+
per_run_hooks: per_run_hooks
|
|
133
|
+
}
|
|
134
|
+
yield
|
|
135
|
+
ensure
|
|
136
|
+
Thread.current[:robot_lab_hook_scope] = previous
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def current_hook_scope
|
|
140
|
+
Thread.current[:robot_lab_hook_scope]
|
|
141
|
+
end
|
|
142
|
+
|
|
84
143
|
# Returns the Config object (MywayConfig-based).
|
|
85
144
|
#
|
|
86
145
|
# Configuration is automatically loaded from:
|
|
@@ -100,7 +159,6 @@ module RobotLab
|
|
|
100
159
|
@config ||= Config.new.tap(&:after_load)
|
|
101
160
|
end
|
|
102
161
|
|
|
103
|
-
|
|
104
162
|
# Yields the Config object for block-style configuration.
|
|
105
163
|
#
|
|
106
164
|
# @yield [Config] the config instance
|
|
@@ -114,7 +172,6 @@ module RobotLab
|
|
|
114
172
|
yield config
|
|
115
173
|
end
|
|
116
174
|
|
|
117
|
-
|
|
118
175
|
# Reload configuration from all sources.
|
|
119
176
|
#
|
|
120
177
|
# Clears the cached Config instance, forcing it to be
|
|
@@ -126,6 +183,23 @@ module RobotLab
|
|
|
126
183
|
config
|
|
127
184
|
end
|
|
128
185
|
|
|
186
|
+
# Render a named prompt template to a String using the configured template
|
|
187
|
+
# library (the prompts_dir from config / ROBOT_LAB_TEMPLATE_PATH). Front
|
|
188
|
+
# matter parameters are supplied as keyword arguments.
|
|
189
|
+
#
|
|
190
|
+
# Unlike #build (which renders a template as a robot's *system prompt*), this
|
|
191
|
+
# returns the plain text, so callers can use it as a task, a message, or a
|
|
192
|
+
# grading rubric.
|
|
193
|
+
#
|
|
194
|
+
# @param name [Symbol, String] template id (filename without extension)
|
|
195
|
+
# @param context [Hash] values for the template's parameters
|
|
196
|
+
# @return [String] the rendered prompt text
|
|
197
|
+
#
|
|
198
|
+
# @example
|
|
199
|
+
# RobotLab.render_template(:objective, topic: "commit messages")
|
|
200
|
+
def render_template(name, **context)
|
|
201
|
+
PM.parse(name.to_sym).to_s(**context)
|
|
202
|
+
end
|
|
129
203
|
|
|
130
204
|
# Factory method to create a new Robot instance.
|
|
131
205
|
#
|
|
@@ -153,7 +227,8 @@ module RobotLab
|
|
|
153
227
|
# name: "helper",
|
|
154
228
|
# system_prompt: "You are a helpful assistant."
|
|
155
229
|
# )
|
|
156
|
-
def build(name: "robot", template: nil, system_prompt: nil, context: {}, enable_cache: true, bus: nil, skills: nil,
|
|
230
|
+
def build(name: "robot", template: nil, system_prompt: nil, context: {}, enable_cache: true, bus: nil, skills: nil,
|
|
231
|
+
config: nil, **)
|
|
157
232
|
Robot.new(
|
|
158
233
|
name: name,
|
|
159
234
|
template: template,
|
|
@@ -163,11 +238,10 @@ module RobotLab
|
|
|
163
238
|
bus: bus,
|
|
164
239
|
skills: skills,
|
|
165
240
|
config: config,
|
|
166
|
-
**
|
|
241
|
+
**
|
|
167
242
|
)
|
|
168
243
|
end
|
|
169
244
|
|
|
170
|
-
|
|
171
245
|
# Factory method to create a new Network of robots.
|
|
172
246
|
#
|
|
173
247
|
# @param name [String] the unique identifier for the network
|
|
@@ -195,11 +269,10 @@ module RobotLab
|
|
|
195
269
|
# step :entities, entity_bot, depends_on: [:fetch]
|
|
196
270
|
# step :merge, merger, depends_on: [:sentiment, :entities]
|
|
197
271
|
# end
|
|
198
|
-
def create_network(name:, concurrency: :auto, config: nil, &
|
|
199
|
-
Network.new(name: name, concurrency: concurrency, config: config, &
|
|
272
|
+
def create_network(name:, concurrency: :auto, config: nil, &)
|
|
273
|
+
Network.new(name: name, concurrency: concurrency, config: config, &)
|
|
200
274
|
end
|
|
201
275
|
|
|
202
|
-
|
|
203
276
|
# Factory method to create a new Memory object.
|
|
204
277
|
#
|
|
205
278
|
# @param data [Hash] initial runtime data
|
|
@@ -216,43 +289,8 @@ module RobotLab
|
|
|
216
289
|
#
|
|
217
290
|
# @example Memory with caching disabled
|
|
218
291
|
# 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
|
|
292
|
+
def create_memory(data: {}, enable_cache: true, **)
|
|
293
|
+
Memory.new(data: data, enable_cache: enable_cache, **)
|
|
245
294
|
end
|
|
246
295
|
end
|
|
247
296
|
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,9 @@ 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
|
+
- Hook System: guides/hooks.md
|
|
174
|
+
- Knowledge Search: guides/knowledge.md
|
|
174
175
|
- API Reference:
|
|
175
176
|
- api/index.md
|
|
176
177
|
- Core Classes:
|
|
@@ -201,4 +202,3 @@ nav:
|
|
|
201
202
|
- Multi-Robot Network: examples/multi-robot-network.md
|
|
202
203
|
- Tool Usage: examples/tool-usage.md
|
|
203
204
|
- 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.
|