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.
Files changed (242) 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/.irbrc +2 -2
  10. data/.rubocop.yml +172 -0
  11. data/CHANGELOG.md +72 -0
  12. data/CLAUDE.md +139 -0
  13. data/README.md +91 -95
  14. data/Rakefile +109 -3
  15. data/agent2agent_review.md +192 -0
  16. data/agentf_improvements.md +253 -0
  17. data/agents.md +14 -0
  18. data/docs/examples/index.md +37 -2
  19. data/docs/getting-started/configuration.md +20 -7
  20. data/docs/guides/index.md +16 -16
  21. data/docs/guides/knowledge.md +7 -1
  22. data/docs/guides/observability.md +132 -0
  23. data/docs/index.md +30 -3
  24. data/docs/superpowers/plans/2026-05-06-agentskills.md +1303 -0
  25. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +247 -0
  26. data/examples/.envrc +1 -0
  27. data/examples/01_simple_robot.rb +5 -9
  28. data/examples/02_tools.rb +5 -9
  29. data/examples/03_network.rb +8 -9
  30. data/examples/04_mcp.rb +21 -29
  31. data/examples/05_streaming.rb +12 -18
  32. data/examples/06_prompt_templates.rb +11 -19
  33. data/examples/07_network_memory.rb +16 -31
  34. data/examples/08_llm_config.rb +10 -22
  35. data/examples/09_chaining.rb +16 -27
  36. data/examples/10_memory.rb +12 -28
  37. data/examples/11_network_introspection.rb +15 -29
  38. data/examples/12_message_bus.rb +5 -12
  39. data/examples/13_spawn.rb +5 -10
  40. data/examples/14_rusty_circuit/.envrc +1 -0
  41. data/examples/14_rusty_circuit/comic.rb +2 -0
  42. data/examples/14_rusty_circuit/heckler.rb +1 -1
  43. data/examples/14_rusty_circuit/open_mic.rb +1 -3
  44. data/examples/14_rusty_circuit/scout.rb +2 -0
  45. data/examples/15_memory_network_and_bus/.envrc +1 -0
  46. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +6 -3
  47. data/examples/15_memory_network_and_bus/linux_writer.rb +1 -1
  48. data/examples/15_memory_network_and_bus/output/combined_article.md +6 -6
  49. data/examples/15_memory_network_and_bus/output/final_article.md +6 -8
  50. data/examples/15_memory_network_and_bus/output/linux_draft.md +4 -2
  51. data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
  52. data/examples/15_memory_network_and_bus/output/memory.json +6 -6
  53. data/examples/15_memory_network_and_bus/output/revision_1.md +10 -11
  54. data/examples/15_memory_network_and_bus/output/revision_2.md +6 -8
  55. data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
  56. data/examples/16_writers_room/.envrc +1 -0
  57. data/examples/16_writers_room/writers_room.rb +2 -4
  58. data/examples/17_skills.rb +8 -17
  59. data/examples/18_rails/Gemfile +1 -0
  60. data/examples/19_token_tracking.rb +9 -15
  61. data/examples/20_circuit_breaker.rb +10 -19
  62. data/examples/21_learning_loop.rb +11 -20
  63. data/examples/22_context_compression.rb +6 -13
  64. data/examples/23_convergence.rb +6 -17
  65. data/examples/24_structured_delegation.rb +11 -15
  66. data/examples/25_history_search.rb +5 -12
  67. data/examples/26_document_store.rb +6 -13
  68. data/examples/27_incident_response/incident_response.rb +4 -5
  69. data/examples/28_mcp_discovery.rb +8 -11
  70. data/examples/29_ractor_tools.rb +4 -9
  71. data/examples/30_ractor_network.rb +10 -19
  72. data/examples/31_launch_assessment.rb +10 -23
  73. data/examples/32_newsletter_reader.rb +188 -0
  74. data/examples/33_stock_generator.rb +80 -0
  75. data/examples/33_stock_predictor.rb +306 -0
  76. data/examples/34_agentskills.rb +72 -0
  77. data/examples/README.md +1 -1
  78. data/examples/common.rb +76 -0
  79. data/examples/ruboruby.md +423 -0
  80. data/examples/temp.md +51 -0
  81. data/lib/robot_lab/agent_skill.rb +63 -0
  82. data/lib/robot_lab/agent_skill_catalog.rb +74 -0
  83. data/lib/robot_lab/ask_user.rb +2 -2
  84. data/lib/robot_lab/bus_poller.rb +12 -5
  85. data/lib/robot_lab/config.rb +1 -12
  86. data/lib/robot_lab/delegation_future.rb +1 -1
  87. data/lib/robot_lab/doom_loop_detector.rb +98 -0
  88. data/lib/robot_lab/history_compressor.rb +4 -10
  89. data/lib/robot_lab/mcp/client.rb +1 -2
  90. data/lib/robot_lab/mcp/connection_poller.rb +3 -3
  91. data/lib/robot_lab/mcp/server.rb +1 -1
  92. data/lib/robot_lab/mcp/server_discovery.rb +0 -2
  93. data/lib/robot_lab/memory.rb +32 -27
  94. data/lib/robot_lab/memory_change.rb +2 -2
  95. data/lib/robot_lab/message.rb +4 -4
  96. data/lib/robot_lab/network.rb +11 -6
  97. data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
  98. data/lib/robot_lab/robot/bus_messaging.rb +9 -27
  99. data/lib/robot_lab/robot/history_search.rb +4 -1
  100. data/lib/robot_lab/robot/mcp_management.rb +5 -11
  101. data/lib/robot_lab/robot/template_rendering.rb +60 -40
  102. data/lib/robot_lab/robot.rb +323 -206
  103. data/lib/robot_lab/robot_result.rb +6 -5
  104. data/lib/robot_lab/run_config.rb +5 -11
  105. data/lib/robot_lab/script_tool.rb +76 -0
  106. data/lib/robot_lab/state_proxy.rb +7 -5
  107. data/lib/robot_lab/tool.rb +3 -3
  108. data/lib/robot_lab/tool_config.rb +1 -1
  109. data/lib/robot_lab/tool_manifest.rb +5 -7
  110. data/lib/robot_lab/user_message.rb +2 -2
  111. data/lib/robot_lab/version.rb +1 -1
  112. data/lib/robot_lab/waiter.rb +1 -1
  113. data/lib/robot_lab.rb +41 -52
  114. data/logfile +8 -0
  115. data/mkdocs.yml +2 -3
  116. data/robot_concurrency.md +38 -0
  117. data/simple_acp_review.md +298 -0
  118. data/site/404.html +2300 -0
  119. data/site/api/core/index.html +2706 -0
  120. data/site/api/core/memory/index.html +3793 -0
  121. data/site/api/core/network/index.html +3500 -0
  122. data/site/api/core/robot/index.html +4566 -0
  123. data/site/api/core/state/index.html +3390 -0
  124. data/site/api/core/tool/index.html +3843 -0
  125. data/site/api/index.html +2635 -0
  126. data/site/api/mcp/client/index.html +3435 -0
  127. data/site/api/mcp/index.html +2783 -0
  128. data/site/api/mcp/server/index.html +3252 -0
  129. data/site/api/mcp/transports/index.html +3352 -0
  130. data/site/api/messages/index.html +2641 -0
  131. data/site/api/messages/text-message/index.html +3087 -0
  132. data/site/api/messages/tool-call-message/index.html +3159 -0
  133. data/site/api/messages/tool-result-message/index.html +3252 -0
  134. data/site/api/messages/user-message/index.html +3212 -0
  135. data/site/api/streaming/context/index.html +3282 -0
  136. data/site/api/streaming/events/index.html +3347 -0
  137. data/site/api/streaming/index.html +2738 -0
  138. data/site/architecture/core-concepts/index.html +3757 -0
  139. data/site/architecture/index.html +2797 -0
  140. data/site/architecture/message-flow/index.html +3238 -0
  141. data/site/architecture/network-orchestration/index.html +3433 -0
  142. data/site/architecture/robot-execution/index.html +3140 -0
  143. data/site/architecture/state-management/index.html +3498 -0
  144. data/site/assets/css/custom.css +56 -0
  145. data/site/assets/images/favicon.png +0 -0
  146. data/site/assets/images/robot_lab.jpg +0 -0
  147. data/site/assets/javascripts/bundle.79ae519e.min.js +16 -0
  148. data/site/assets/javascripts/bundle.79ae519e.min.js.map +7 -0
  149. data/site/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
  150. data/site/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
  151. data/site/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
  152. data/site/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
  153. data/site/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
  154. data/site/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
  155. data/site/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
  156. data/site/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
  157. data/site/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
  158. data/site/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
  159. data/site/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
  160. data/site/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
  161. data/site/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
  162. data/site/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
  163. data/site/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
  164. data/site/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
  165. data/site/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
  166. data/site/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
  167. data/site/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
  168. data/site/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
  169. data/site/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
  170. data/site/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
  171. data/site/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
  172. data/site/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
  173. data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
  174. data/site/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
  175. data/site/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
  176. data/site/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
  177. data/site/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
  178. data/site/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
  179. data/site/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
  180. data/site/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
  181. data/site/assets/javascripts/lunr/tinyseg.js +206 -0
  182. data/site/assets/javascripts/lunr/wordcut.js +6708 -0
  183. data/site/assets/javascripts/workers/search.2c215733.min.js +42 -0
  184. data/site/assets/javascripts/workers/search.2c215733.min.js.map +7 -0
  185. data/site/assets/stylesheets/main.484c7ddc.min.css +1 -0
  186. data/site/assets/stylesheets/main.484c7ddc.min.css.map +1 -0
  187. data/site/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
  188. data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +1 -0
  189. data/site/concepts/index.html +3455 -0
  190. data/site/examples/basic-chat/index.html +2880 -0
  191. data/site/examples/index.html +2907 -0
  192. data/site/examples/mcp-server/index.html +3018 -0
  193. data/site/examples/multi-robot-network/index.html +3131 -0
  194. data/site/examples/rails-application/index.html +3329 -0
  195. data/site/examples/tool-usage/index.html +3085 -0
  196. data/site/getting-started/configuration/index.html +3745 -0
  197. data/site/getting-started/index.html +2572 -0
  198. data/site/getting-started/installation/index.html +2981 -0
  199. data/site/getting-started/quick-start/index.html +2942 -0
  200. data/site/guides/building-robots/index.html +4290 -0
  201. data/site/guides/creating-networks/index.html +3858 -0
  202. data/site/guides/index.html +2586 -0
  203. data/site/guides/mcp-integration/index.html +3581 -0
  204. data/site/guides/memory/index.html +3586 -0
  205. data/site/guides/rails-integration/index.html +4019 -0
  206. data/site/guides/streaming/index.html +3157 -0
  207. data/site/guides/using-tools/index.html +3802 -0
  208. data/site/index.html +2671 -0
  209. data/site/search/search_index.json +1 -0
  210. data/site/sitemap.xml +183 -0
  211. data/site/sitemap.xml.gz +0 -0
  212. data/site/tags.json +1 -0
  213. data/temp.md +6 -0
  214. data/tool_manifest_plan.md +155 -0
  215. metadata +154 -92
  216. data/docs/examples/rails-application.md +0 -419
  217. data/docs/guides/ractor-parallelism.md +0 -364
  218. data/docs/guides/rails-integration.md +0 -681
  219. data/docs/superpowers/plans/2026-04-14-ractor-integration.md +0 -1538
  220. data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +0 -258
  221. data/lib/generators/robot_lab/install_generator.rb +0 -90
  222. data/lib/generators/robot_lab/job_generator.rb +0 -40
  223. data/lib/generators/robot_lab/robot_generator.rb +0 -55
  224. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -42
  225. data/lib/generators/robot_lab/templates/job.rb.tt +0 -21
  226. data/lib/generators/robot_lab/templates/migration.rb.tt +0 -32
  227. data/lib/generators/robot_lab/templates/result_model.rb.tt +0 -52
  228. data/lib/generators/robot_lab/templates/robot.rb.tt +0 -31
  229. data/lib/generators/robot_lab/templates/robot_job.rb.tt +0 -18
  230. data/lib/generators/robot_lab/templates/robot_test.rb.tt +0 -34
  231. data/lib/generators/robot_lab/templates/routing_robot.rb.tt +0 -59
  232. data/lib/generators/robot_lab/templates/thread_model.rb.tt +0 -40
  233. data/lib/robot_lab/document_store.rb +0 -155
  234. data/lib/robot_lab/ractor_boundary.rb +0 -42
  235. data/lib/robot_lab/ractor_job.rb +0 -37
  236. data/lib/robot_lab/ractor_memory_proxy.rb +0 -85
  237. data/lib/robot_lab/ractor_network_scheduler.rb +0 -154
  238. data/lib/robot_lab/ractor_worker_pool.rb +0 -117
  239. data/lib/robot_lab/rails_integration/engine.rb +0 -29
  240. data/lib/robot_lab/rails_integration/job.rb +0 -158
  241. data/lib/robot_lab/rails_integration/railtie.rb +0 -51
  242. 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(*args)
147
- export.to_json(*args)
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.reverse.find(&:text?)&.content
155
+ output.rfind(&:text?)&.content
156
156
  end
157
- alias_method :reply, :last_text_content
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, :stop_reason)) : Message.from_hash(result)
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
@@ -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].freeze
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
- NON_SERIALIZABLE_FIELDS = (CALLBACK_FIELDS + %i[bus]).freeze
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.reject { |k, _| NON_SERIALIZABLE_FIELDS.include?(k) }
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(&block)
89
- @data.each(&block)
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, &block)
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
@@ -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(*args)
208
- to_h.to_json(*args)
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.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.1"
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
@@ -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, config: nil, **options)
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
- **options
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, &block)
199
- Network.new(name: name, concurrency: concurrency, config: config, &block)
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, **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
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
- - Rails Integration: guides/rails-integration.md
173
- - Ractor Parallelism: guides/ractor-parallelism.md
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.