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.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/.architecture/AGENTS.md +32 -0
  3. data/.architecture/config.yml +8 -0
  4. data/.architecture/members.yml +60 -0
  5. data/.architecture/reviews/feature-free-will.md +490 -0
  6. data/.architecture/reviews/overall-codebase.md +427 -0
  7. data/.claude/settings.local.json +57 -0
  8. data/.codex/config.toml +2 -0
  9. data/.envrc +1 -0
  10. data/.irbrc +2 -2
  11. data/.loki +60 -0
  12. data/.quality/reek_baseline.txt +43 -0
  13. data/.rubocop.yml +10 -0
  14. data/CHANGELOG.md +126 -0
  15. data/CLAUDE.md +139 -0
  16. data/README.md +130 -73
  17. data/Rakefile +131 -9
  18. data/agent2agent_review.md +192 -0
  19. data/agentf_improvements.md +253 -0
  20. data/agents.md +14 -0
  21. data/docs/api/core/index.md +1 -0
  22. data/docs/api/core/network.md +31 -1
  23. data/docs/api/core/robot.md +91 -5
  24. data/docs/api/errors.md +47 -2
  25. data/docs/api/index.md +3 -0
  26. data/docs/architecture/core-concepts.md +40 -0
  27. data/docs/architecture/network-orchestration.md +8 -0
  28. data/docs/architecture/robot-execution.md +1 -1
  29. data/docs/examples/index.md +37 -2
  30. data/docs/getting-started/configuration.md +39 -7
  31. data/docs/guides/building-robots.md +39 -1
  32. data/docs/guides/creating-networks.md +6 -1
  33. data/docs/guides/hooks.md +997 -0
  34. data/docs/guides/index.md +19 -14
  35. data/docs/guides/knowledge.md +9 -3
  36. data/docs/guides/observability.md +206 -7
  37. data/docs/guides/using-tools.md +69 -0
  38. data/docs/index.md +31 -4
  39. data/docs/superpowers/plans/2026-05-06-agentskills.md +1303 -0
  40. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +247 -0
  41. data/examples/.envrc +1 -0
  42. data/examples/01_simple_robot.rb +5 -9
  43. data/examples/02_tools.rb +5 -9
  44. data/examples/03_network.rb +18 -10
  45. data/examples/04_mcp.rb +21 -29
  46. data/examples/05_streaming.rb +12 -18
  47. data/examples/06_prompt_templates.rb +11 -19
  48. data/examples/07_network_memory.rb +16 -31
  49. data/examples/08_llm_config.rb +10 -22
  50. data/examples/09_chaining.rb +16 -27
  51. data/examples/10_memory.rb +12 -28
  52. data/examples/11_network_introspection.rb +15 -29
  53. data/examples/12_message_bus.rb +5 -12
  54. data/examples/13_spawn.rb +5 -10
  55. data/examples/14_rusty_circuit/.envrc +1 -0
  56. data/examples/14_rusty_circuit/comic.rb +2 -0
  57. data/examples/14_rusty_circuit/heckler.rb +1 -1
  58. data/examples/14_rusty_circuit/open_mic.rb +1 -3
  59. data/examples/14_rusty_circuit/scout.rb +2 -0
  60. data/examples/15_memory_network_and_bus/.envrc +1 -0
  61. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +6 -3
  62. data/examples/15_memory_network_and_bus/linux_writer.rb +1 -1
  63. data/examples/15_memory_network_and_bus/output/combined_article.md +6 -6
  64. data/examples/15_memory_network_and_bus/output/final_article.md +6 -8
  65. data/examples/15_memory_network_and_bus/output/linux_draft.md +4 -2
  66. data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
  67. data/examples/15_memory_network_and_bus/output/memory.json +6 -6
  68. data/examples/15_memory_network_and_bus/output/revision_1.md +10 -11
  69. data/examples/15_memory_network_and_bus/output/revision_2.md +6 -8
  70. data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
  71. data/examples/16_writers_room/.envrc +1 -0
  72. data/examples/16_writers_room/writers_room.rb +2 -4
  73. data/examples/17_skills.rb +8 -17
  74. data/examples/18_rails/Gemfile +1 -0
  75. data/examples/19_token_tracking.rb +9 -15
  76. data/examples/20_circuit_breaker.rb +10 -19
  77. data/examples/21_learning_loop.rb +11 -20
  78. data/examples/22_context_compression.rb +6 -13
  79. data/examples/23_convergence.rb +6 -17
  80. data/examples/24_structured_delegation.rb +11 -15
  81. data/examples/25_history_search.rb +5 -12
  82. data/examples/26_document_store.rb +6 -13
  83. data/examples/27_incident_response/incident_response.rb +4 -5
  84. data/examples/28_mcp_discovery.rb +8 -11
  85. data/examples/29_ractor_tools.rb +4 -9
  86. data/examples/30_ractor_network.rb +10 -19
  87. data/examples/31_launch_assessment.rb +10 -23
  88. data/examples/32_newsletter_reader.rb +188 -0
  89. data/examples/33_stock_generator.rb +80 -0
  90. data/examples/33_stock_predictor.rb +306 -0
  91. data/examples/34_agentskills.rb +72 -0
  92. data/examples/35_hooks.rb +256 -0
  93. data/examples/README.md +8 -1
  94. data/examples/common.rb +80 -0
  95. data/examples/ruboruby.md +423 -0
  96. data/examples/xyzzy.rb +90 -0
  97. data/lib/robot_lab/agent_skill.rb +64 -0
  98. data/lib/robot_lab/agent_skill_catalog.rb +74 -0
  99. data/lib/robot_lab/ask_user.rb +2 -2
  100. data/lib/robot_lab/budget/ledger.rb +98 -0
  101. data/lib/robot_lab/bus_poller.rb +12 -5
  102. data/lib/robot_lab/capabilities.rb +84 -0
  103. data/lib/robot_lab/config/defaults.yml +10 -0
  104. data/lib/robot_lab/config.rb +5 -16
  105. data/lib/robot_lab/delegation_future.rb +1 -1
  106. data/lib/robot_lab/doom_loop_detector.rb +98 -0
  107. data/lib/robot_lab/error.rb +34 -4
  108. data/lib/robot_lab/errors.rb +45 -0
  109. data/lib/robot_lab/history_compressor.rb +4 -10
  110. data/lib/robot_lab/hook.rb +79 -0
  111. data/lib/robot_lab/hook_context.rb +194 -0
  112. data/lib/robot_lab/hook_registry.rb +55 -0
  113. data/lib/robot_lab/hooks.rb +87 -0
  114. data/lib/robot_lab/mcp/client.rb +1 -2
  115. data/lib/robot_lab/mcp/connection_poller.rb +5 -5
  116. data/lib/robot_lab/mcp/server.rb +1 -1
  117. data/lib/robot_lab/mcp/server_discovery.rb +0 -2
  118. data/lib/robot_lab/memory.rb +32 -27
  119. data/lib/robot_lab/memory_change.rb +2 -2
  120. data/lib/robot_lab/message.rb +4 -4
  121. data/lib/robot_lab/narrator.rb +87 -0
  122. data/lib/robot_lab/network.rb +87 -25
  123. data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
  124. data/lib/robot_lab/robot/budget.rb +89 -0
  125. data/lib/robot_lab/robot/bus_messaging.rb +79 -33
  126. data/lib/robot_lab/robot/history_search.rb +4 -1
  127. data/lib/robot_lab/robot/hooking.rb +56 -0
  128. data/lib/robot_lab/robot/mcp_management.rb +12 -12
  129. data/lib/robot_lab/robot/template_rendering.rb +62 -42
  130. data/lib/robot_lab/robot.rb +482 -242
  131. data/lib/robot_lab/robot_result.rb +6 -5
  132. data/lib/robot_lab/run_config.rb +27 -14
  133. data/lib/robot_lab/runnable.rb +51 -0
  134. data/lib/robot_lab/sandbox/null.rb +13 -0
  135. data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
  136. data/lib/robot_lab/sandbox.rb +52 -0
  137. data/lib/robot_lab/script_tool.rb +134 -0
  138. data/lib/robot_lab/state_proxy.rb +7 -5
  139. data/lib/robot_lab/task.rb +26 -20
  140. data/lib/robot_lab/tool.rb +54 -13
  141. data/lib/robot_lab/tool_config.rb +1 -1
  142. data/lib/robot_lab/tool_manifest.rb +5 -7
  143. data/lib/robot_lab/user_message.rb +2 -2
  144. data/lib/robot_lab/version.rb +1 -1
  145. data/lib/robot_lab/waiter.rb +1 -1
  146. data/lib/robot_lab.rb +89 -51
  147. data/logfile +8 -0
  148. data/mkdocs.yml +3 -3
  149. data/robot_concurrency.md +38 -0
  150. data/simple_acp_review.md +298 -0
  151. data/temp.md +6 -0
  152. data/tool_manifest_plan.md +155 -0
  153. metadata +82 -83
  154. data/docs/examples/rails-application.md +0 -419
  155. data/docs/guides/ractor-parallelism.md +0 -364
  156. data/docs/guides/rails-integration.md +0 -681
  157. data/docs/superpowers/plans/2026-04-14-ractor-integration.md +0 -1538
  158. data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +0 -258
  159. data/lib/generators/robot_lab/install_generator.rb +0 -90
  160. data/lib/generators/robot_lab/job_generator.rb +0 -40
  161. data/lib/generators/robot_lab/robot_generator.rb +0 -55
  162. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -42
  163. data/lib/generators/robot_lab/templates/job.rb.tt +0 -21
  164. data/lib/generators/robot_lab/templates/migration.rb.tt +0 -32
  165. data/lib/generators/robot_lab/templates/result_model.rb.tt +0 -52
  166. data/lib/generators/robot_lab/templates/robot.rb.tt +0 -31
  167. data/lib/generators/robot_lab/templates/robot_job.rb.tt +0 -18
  168. data/lib/generators/robot_lab/templates/robot_test.rb.tt +0 -34
  169. data/lib/generators/robot_lab/templates/routing_robot.rb.tt +0 -59
  170. data/lib/generators/robot_lab/templates/thread_model.rb.tt +0 -40
  171. data/lib/robot_lab/document_store.rb +0 -155
  172. data/lib/robot_lab/ractor_boundary.rb +0 -42
  173. data/lib/robot_lab/ractor_job.rb +0 -37
  174. data/lib/robot_lab/ractor_memory_proxy.rb +0 -85
  175. data/lib/robot_lab/ractor_network_scheduler.rb +0 -154
  176. data/lib/robot_lab/ractor_worker_pool.rb +0 -117
  177. data/lib/robot_lab/rails_integration/engine.rb +0 -29
  178. data/lib/robot_lab/rails_integration/job.rb +0 -158
  179. data/lib/robot_lab/rails_integration/railtie.rb +0 -51
  180. data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +0 -72
@@ -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
- if self.class.ractor_safe? && !self.class.name.nil?
98
- RobotLab.ractor_pool.submit(self.class.name, args)
99
- else
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
- RobotLab.config.logger.warn("Tool '#{name}' error: #{e.class}: #{e.message}")
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(*args)
208
- to_h.to_json(*args)
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.map(&:to_s).to_set
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(&block)
146
- @tools.values.each(&block)
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(*args)
198
- to_h.to_json(*args)
195
+ def to_json(*)
196
+ to_h.to_json(*)
199
197
  end
200
198
 
201
199
  # Create manifest from hash of tool definitions
@@ -84,8 +84,8 @@ module RobotLab
84
84
  #
85
85
  # @param args [Array] arguments passed to to_json
86
86
  # @return [String] JSON representation
87
- def to_json(*args)
88
- to_h.to_json(*args)
87
+ def to_json(*)
88
+ to_h.to_json(*)
89
89
  end
90
90
 
91
91
  # Create from string or hash
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RobotLab
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.6"
5
5
  end
@@ -36,7 +36,7 @@ module RobotLab
36
36
  end
37
37
 
38
38
  begin
39
- ready = IO.select([@read_io], nil, nil, timeout)
39
+ ready = @read_io.wait_readable(timeout)
40
40
 
41
41
  @mutex.synchronize do
42
42
  @waiter_count -= 1
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, config: nil, **options)
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
- **options
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, &block)
199
- Network.new(name: name, concurrency: concurrency, config: config, &block)
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, **options)
220
- Memory.new(data: data, enable_cache: enable_cache, **options)
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
- - Rails Integration: guides/rails-integration.md
173
- - Ractor Parallelism: guides/ractor-parallelism.md
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.