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
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RobotLab
4
+ class Robot < RubyLLM::Agent
5
+ # Prepended module that intercepts run() to inject relevant AgentSkills.io
6
+ # skills into the system prompt and tool list for the duration of each call.
7
+ #
8
+ # Owns: @_active_agent_skills, @_agent_skill_original_instructions, @_agent_skill_injected_tools
9
+ # Reads: @pending_agent_skills, @agent_skill_store, @local_tools, @chat, @name
10
+ # Contract: prepended (wraps super); requires TemplateRendering to have set @pending_agent_skills
11
+ #
12
+ # Skills are matched by embedding similarity between the incoming message
13
+ # and each pending skill's description (via DocumentStore/fastembed).
14
+ # Injected content is fully restored in an ensure block after run() returns.
15
+ module AgentSkillMatching
16
+ SIMILARITY_THRESHOLD = 0.70
17
+
18
+ def run(message = nil, **, &)
19
+ matched = match_agent_skills(message.to_s)
20
+ inject_agent_skills(matched) if matched.any?
21
+ super
22
+ ensure
23
+ restore_after_agent_skills if @_active_agent_skills&.any?
24
+ end
25
+
26
+ # Override to re-inject skill instructions after template re-render replaces
27
+ # the system prompt during a run() call with runtime kwargs.
28
+ def rerender_template(run_context)
29
+ super
30
+ return unless @_active_agent_skills&.any?
31
+
32
+ @_agent_skill_original_instructions = current_agent_skill_instructions
33
+ prepend_skill_instructions(@_active_agent_skills)
34
+ end
35
+
36
+ private
37
+
38
+ # Find pending AgentSkills whose descriptions are semantically similar to message.
39
+ #
40
+ # @param message [String]
41
+ # @param threshold [Float] cosine similarity cutoff (default SIMILARITY_THRESHOLD)
42
+ # @return [Array<AgentSkill>]
43
+ def match_agent_skills(message, threshold: SIMILARITY_THRESHOLD)
44
+ return [] if @pending_agent_skills.nil? || @pending_agent_skills.empty?
45
+
46
+ results = @agent_skill_store.search(message, limit: @pending_agent_skills.size)
47
+ results
48
+ .select { |r| r[:score] >= threshold }
49
+ .filter_map { |r| @pending_agent_skills.find { |s| s.name.to_sym == r[:key] } }
50
+ rescue => e
51
+ RobotLab.config.logger.warn(
52
+ "Robot '#{@name}': AgentSkill embedding failed: #{e.message}"
53
+ )
54
+ []
55
+ end
56
+
57
+ # Prepend skill instructions before existing system prompt content.
58
+ #
59
+ # @param skills [Array<AgentSkill>]
60
+ def prepend_skill_instructions(skills)
61
+ skill_content = skills.map(&:instructions).join("\n\n")
62
+ base = @_agent_skill_original_instructions.to_s
63
+ combined = [skill_content, base].reject(&:empty?).join("\n\n")
64
+ @chat.with_instructions(combined)
65
+ end
66
+
67
+ # Inject script tools and snapshot instructions before injection.
68
+ #
69
+ # @param skills [Array<AgentSkill>]
70
+ def inject_agent_skills(skills)
71
+ return if skills.empty?
72
+
73
+ @_active_agent_skills = skills
74
+ @_agent_skill_original_instructions = current_agent_skill_instructions
75
+ prepend_skill_instructions(skills)
76
+ @_agent_skill_injected_tools = skills.flat_map(&:script_tools).compact
77
+ @local_tools += @_agent_skill_injected_tools
78
+ end
79
+
80
+ # Remove injected tools and restore original system prompt.
81
+ def restore_after_agent_skills
82
+ @local_tools -= @_agent_skill_injected_tools || []
83
+ @chat.with_instructions(@_agent_skill_original_instructions.to_s)
84
+ @_active_agent_skills = nil
85
+ @_agent_skill_original_instructions = nil
86
+ @_agent_skill_injected_tools = nil
87
+ end
88
+
89
+ # Read the current system message content from the underlying chat.
90
+ #
91
+ # @return [String, nil]
92
+ def current_agent_skill_instructions
93
+ messages = @chat.instance_variable_get(:@messages)
94
+ sys = messages&.find { |m| m.role.to_s == "system" }
95
+ sys&.content
96
+ end
97
+ end
98
+ end
99
+ end
@@ -16,6 +16,9 @@ module RobotLab
16
16
  # inline. The BusPoller drains each group's queue sequentially on
17
17
  # a dedicated OS thread, so robot.run() calls never interleave.
18
18
  #
19
+ # Owns: @bus, @bus_poller, @private_bus_poller, @bus_poller_group, @bus_subscriber_id, @message_counter, @outbox, @message_handler
20
+ # Reads: @name
21
+ # Contract: ivars initialized by initialize_runtime_state before first bus operation
19
22
  module BusMessaging
20
23
  # Send a message to another robot via the bus.
21
24
  #
@@ -33,7 +36,6 @@ module RobotLab
33
36
  message
34
37
  end
35
38
 
36
-
37
39
  # Send a reply to a specific message via the bus.
38
40
  #
39
41
  # @param to [String, Symbol] target robot's channel name
@@ -50,7 +52,6 @@ module RobotLab
50
52
  reply
51
53
  end
52
54
 
53
-
54
55
  # Register a custom handler for incoming bus messages.
55
56
  #
56
57
  # Block arity controls delivery handling:
@@ -64,7 +65,6 @@ module RobotLab
64
65
  self
65
66
  end
66
67
 
67
-
68
68
  # Spawn a new robot on a shared bus.
69
69
  #
70
70
  # Creates a new Robot instance that shares this robot's bus,
@@ -79,7 +79,7 @@ module RobotLab
79
79
  # @param options [Hash] additional options passed to RobotLab.build
80
80
  # @return [Robot] the newly created robot
81
81
  #
82
- def spawn(name: "robot", system_prompt: nil, template: nil, local_tools: [], **options)
82
+ def spawn(name: "robot", system_prompt: nil, template: nil, local_tools: [], **)
83
83
  ensure_bus
84
84
 
85
85
  RobotLab.build(
@@ -88,11 +88,10 @@ module RobotLab
88
88
  template: template,
89
89
  local_tools: local_tools,
90
90
  bus: @bus,
91
- **options
91
+ **
92
92
  )
93
93
  end
94
94
 
95
-
96
95
  # Connect this robot to a message bus.
97
96
  #
98
97
  # If a bus is provided, the robot joins it. If no bus is provided
@@ -134,7 +133,6 @@ module RobotLab
134
133
  with_bus unless @bus
135
134
  end
136
135
 
137
-
138
136
  # Create a typed channel on the bus and subscribe to it.
139
137
  # Auto-creates a private BusPoller if none has been assigned.
140
138
  def setup_bus_channel
@@ -149,7 +147,6 @@ module RobotLab
149
147
  @bus_subscriber_id = @bus.subscribe(channel_name) { |delivery| enqueue_delivery(delivery) }
150
148
  end
151
149
 
152
-
153
150
  # Unsubscribe from the bus channel and stop the private poller if any.
154
151
  def teardown_bus_channel
155
152
  channel_name = @name.to_sym
@@ -162,13 +159,11 @@ module RobotLab
162
159
  @bus_poller_group = :default
163
160
  end
164
161
 
165
-
166
162
  # Enqueue a delivery to the robot's assigned poller.
167
163
  def enqueue_delivery(delivery)
168
164
  @bus_poller.enqueue(robot: self, delivery: delivery, group: @bus_poller_group)
169
165
  end
170
166
 
171
-
172
167
  # Process a single delivery (called by BusPoller drain thread).
173
168
  def process_delivery(delivery)
174
169
  message = delivery.message
@@ -180,30 +175,17 @@ module RobotLab
180
175
  entry[:replies] << message
181
176
  end
182
177
 
183
- if @message_handler
184
- if @message_handler.arity == 1
185
- delivery.ack!
186
- @message_handler.call(message)
187
- else
188
- @message_handler.call(delivery, message)
189
- end
178
+ if @message_handler.arity == 1
179
+ delivery.ack!
180
+ @message_handler.call(message)
190
181
  else
191
- handle_message_via_llm(delivery, message)
182
+ @message_handler.call(delivery, message)
192
183
  end
193
184
  rescue => e
194
185
  delivery.nack! if delivery.pending?
195
186
  raise BusError, "Error handling bus message on robot '#{@name}': #{e.message}"
196
187
  end
197
188
 
198
-
199
- # Default handler: interpret message via LLM and reply
200
- def handle_message_via_llm(delivery, message)
201
- delivery.ack!
202
- result = run(message.content.to_s)
203
- send_reply(to: message.from.to_sym, content: result.last_text_content, in_reply_to: message.key)
204
- end
205
-
206
-
207
189
  # Publish a RobotMessage to a bus channel
208
190
  def publish_to_bus(channel_name, message)
209
191
  if defined?(Async::Task) && Async::Task.current?
@@ -4,6 +4,10 @@ module RobotLab
4
4
  class Robot
5
5
  # Semantic search over a robot's conversation history.
6
6
  #
7
+ # Owns: nothing (read-only mixin)
8
+ # Reads: @chat (specifically @chat.messages)
9
+ # Contract: no initialization required; safe to call any time after initialize
10
+ #
7
11
  # Scores each message in @chat.messages against the query using stemmed
8
12
  # term-frequency cosine similarity (via the +classifier+ gem). Returns the
9
13
  # top-N messages ranked by relevance.
@@ -61,7 +65,6 @@ module RobotLab
61
65
  case content
62
66
  when String then content
63
67
  when Array then content.filter_map { |p| p[:text] || p["text"] }.join(" ")
64
- else nil
65
68
  end
66
69
  end
67
70
  end
@@ -4,9 +4,9 @@ module RobotLab
4
4
  class Robot < RubyLLM::Agent
5
5
  # MCP client lifecycle and hierarchical tool/MCP resolution.
6
6
  #
7
- # Expects the including class to provide:
8
- # @mcp_config, @tools_config, @mcp_clients, @mcp_tools,
9
- # @mcp_initialized, @name, @chat, @local_tools
7
+ # Owns: @mcp_clients, @mcp_tools, @mcp_initialized, @failed_mcp_configs
8
+ # Reads: @mcp_config, @tools_config, @name, @chat, @local_tools
9
+ # Contract: ivars initialized by initialize_runtime_state; lazy init on first run
10
10
  module MCPManagement
11
11
  private
12
12
 
@@ -17,7 +17,6 @@ module RobotLab
17
17
  ToolConfig.resolve_mcp(runtime_value, parent_value: build_resolved)
18
18
  end
19
19
 
20
-
21
20
  # Resolve tools hierarchy: runtime -> robot build -> network -> config
22
21
  def resolve_tools_hierarchy(runtime_value, network: nil, network_config: nil)
23
22
  parent_value = network_config&.tools || network&.network&.tools || RobotLab.config.tools
@@ -25,7 +24,6 @@ module RobotLab
25
24
  ToolConfig.resolve_tools(runtime_value, parent_value: build_resolved)
26
25
  end
27
26
 
28
-
29
27
  # Ensure MCP clients are initialized for the given server configs.
30
28
  # On subsequent calls, retries any servers that previously failed to connect.
31
29
  def ensure_mcp_clients(mcp_servers)
@@ -50,7 +48,6 @@ module RobotLab
50
48
  @mcp_initialized = true
51
49
  end
52
50
 
53
-
54
51
  def init_mcp_client(server_config)
55
52
  client = MCP::Client.new(server_config)
56
53
  client.connect
@@ -74,13 +71,12 @@ module RobotLab
74
71
  )
75
72
  end
76
73
 
77
-
78
74
  # Retry connecting to servers that previously failed
79
- def retry_failed_servers(mcp_servers, needed_servers)
75
+ def retry_failed_servers(_mcp_servers, needed_servers)
80
76
  return if @failed_mcp_configs.nil? || @failed_mcp_configs.empty?
81
77
 
82
78
  # Only retry servers that are still needed and still failed
83
- to_retry = @failed_mcp_configs.select { |name, _| needed_servers.include?(name) }
79
+ to_retry = @failed_mcp_configs.slice(*needed_servers)
84
80
  return if to_retry.empty?
85
81
 
86
82
  to_retry.each do |name, server_config|
@@ -106,7 +102,6 @@ module RobotLab
106
102
  end
107
103
  end
108
104
 
109
-
110
105
  def discover_mcp_tools(client, server_name)
111
106
  tools = client.list_tools
112
107
 
@@ -129,7 +124,6 @@ module RobotLab
129
124
  )
130
125
  end
131
126
 
132
-
133
127
  def extract_server_name(server_config)
134
128
  case server_config
135
129
  when Hash
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
-
5
3
  module RobotLab
6
4
  class Robot < RubyLLM::Agent
7
5
  # Template loading, rendering, and front-matter extraction.
8
6
  #
9
- # Expects the including class to provide:
10
- # @chat, @template, @build_context, @name, @name_from_constructor,
11
- # @description, @local_tools, @mcp_config
7
+ # Owns: @expanded_skills, @pending_agent_skills, @agent_skill_store
8
+ # Reads: @chat, @template, @build_context, @name, @name_from_constructor, @description, @local_tools, @mcp_config, @config
9
+ # Contract: called during initialize after assign_identity_ivars and build_effective_config
12
10
  module TemplateRendering
13
11
  # Front matter keys that map to chat configuration methods
14
12
  FRONT_MATTER_CONFIG_KEYS = %i[
@@ -64,7 +62,6 @@ module RobotLab
64
62
  end
65
63
  end
66
64
 
67
-
68
65
  # Re-render the template with run-time context merged into build-time context.
69
66
  # prompt_manager parameters may be required (null) and only available at run time.
70
67
  def rerender_template(run_context)
@@ -93,64 +90,85 @@ module RobotLab
93
90
  end
94
91
  end
95
92
 
96
-
97
93
  # Orchestrate skill expansion and template application.
98
94
  #
99
95
  # @param skill_ids [Array<Symbol>] skill IDs from constructor + front matter
100
96
  # @param context [Hash, Proc] variables to pass to all templates
101
97
  def apply_skills_and_template_to_chat(skill_ids, context)
98
+ bodies, accumulated_config, extras = collect_prompt_content(skill_ids, context)
99
+ apply_prompt_to_chat(bodies, accumulated_config, extras)
100
+ end
101
+
102
+ # Expand skills and render all bodies, configs, and extras into plain data.
103
+ # Pure computation — reads ivars but does not mutate @chat.
104
+ #
105
+ # @return [Array(Array<String>, RunConfig, Hash)] bodies, merged config, extras hash
106
+ def collect_prompt_content(skill_ids, context)
102
107
  visited = Set.new
103
- # Prevent skills from pulling in the main template
104
108
  visited.add(@template) if @template
105
-
106
109
  @expanded_skills = expand_skills(skill_ids, visited)
107
110
 
108
111
  extras = {}
109
112
  accumulated_config = RunConfig.new
110
113
  bodies = []
111
-
112
114
  resolved_ctx = resolve_context(context, network: nil)
113
115
 
114
- # Process each expanded skill
115
116
  @expanded_skills.each do |skill_id|
116
117
  parsed = PM.parse(skill_id)
117
118
  accumulate_extras(parsed.metadata, extras)
118
- fm_config = RunConfig.from_front_matter(parsed.metadata)
119
- accumulated_config = accumulated_config.merge(fm_config)
119
+ accumulated_config = accumulated_config.merge(RunConfig.from_front_matter(parsed.metadata))
120
120
  body = render_body(parsed, resolved_ctx)
121
121
  bodies << body if body
122
122
  end
123
123
 
124
- # Process main template if present
125
124
  if @template
126
125
  parsed = PM.parse(@template)
127
126
  accumulate_extras(parsed.metadata, extras)
128
- fm_config = RunConfig.from_front_matter(parsed.metadata)
129
- accumulated_config = accumulated_config.merge(fm_config)
127
+ accumulated_config = accumulated_config.merge(RunConfig.from_front_matter(parsed.metadata))
130
128
  body = render_body(parsed, resolved_ctx)
131
129
  bodies << body if body
132
130
  end
133
131
 
134
- # Apply accumulated extras (respects constructor precedence)
132
+ [bodies, accumulated_config, extras]
133
+ end
134
+
135
+ # Apply collected prompt content to @chat.
136
+ # Pure mutation — takes plain data and writes to @chat.
137
+ #
138
+ # @param bodies [Array<String>] rendered template bodies
139
+ # @param accumulated_config [RunConfig] merged skill + template config
140
+ # @param extras [Hash] accumulated front-matter extras (name, description, tools, mcp)
141
+ def apply_prompt_to_chat(bodies, accumulated_config, extras)
135
142
  apply_accumulated_extras(extras)
136
143
 
137
- # Constructor config overrides skill + template config
138
144
  effective = accumulated_config.merge(@config)
139
145
  effective.apply_to(@chat)
140
146
 
141
- # Set instructions once with all bodies joined
142
147
  combined = bodies.join("\n\n")
143
148
  @chat.with_instructions(combined) unless combined.empty?
144
149
  end
145
150
 
146
-
147
151
  # Recursively expand skill IDs depth-first.
152
+ # Checks AgentSkillCatalog first; falls back to PM template lookup.
148
153
  # Returns a flat Array<Symbol> in processing order (deepest first).
149
154
  #
150
155
  # @param skill_ids [Array<Symbol>] skill IDs to expand
151
156
  # @param visited [Set<Symbol>] already-visited IDs for cycle detection
152
157
  # @return [Array<Symbol>] flat ordered list of skill IDs
153
158
  def expand_skills(skill_ids, visited = Set.new)
159
+ expand_skills_with_catalog(skill_ids, visited, AgentSkillCatalog.instance)
160
+ end
161
+
162
+ # Recursively expand skill IDs depth-first, using the given catalog.
163
+ # AgentSkills folder format takes priority over PM template lookup.
164
+ # Catalog hits are stored in @pending_agent_skills / @agent_skill_store
165
+ # and are NOT included in the returned Array (they are handled separately).
166
+ #
167
+ # @param skill_ids [Array<Symbol>] skill IDs to expand
168
+ # @param visited [Set<Symbol>] already-visited IDs for cycle detection
169
+ # @param catalog [AgentSkillCatalog] catalog to check first
170
+ # @return [Array<Symbol>] flat ordered list of PM-based skill IDs
171
+ def expand_skills_with_catalog(skill_ids, visited, catalog)
154
172
  result = []
155
173
 
156
174
  skill_ids.each do |skill_id|
@@ -165,21 +183,32 @@ module RobotLab
165
183
 
166
184
  visited.add(skill_id)
167
185
 
168
- # Parse the skill template and check for nested skills
186
+ # Check catalog first: AgentSkills folder format takes priority
187
+ if (agent_skill = catalog.find(skill_id))
188
+ @pending_agent_skills ||= []
189
+ unless defined?(RobotLab::DocumentStore)
190
+ raise LoadError,
191
+ "robot_lab-document_store is required to use AgentSkill catalogs. " \
192
+ "Add `gem 'robot_lab-document_store'` to your Gemfile."
193
+ end
194
+ @agent_skill_store ||= RobotLab::DocumentStore.new
195
+ @pending_agent_skills << agent_skill
196
+ @agent_skill_store.store(agent_skill.name.to_sym, agent_skill.description)
197
+ next
198
+ end
199
+
200
+ # Fall back to PM template (existing behavior)
169
201
  parsed = PM.parse(skill_id)
170
202
  nested = extract_skills_from_metadata(parsed.metadata)
171
203
 
172
- # Recurse on nested skills first (depth-first)
173
- result.concat(expand_skills(nested, visited)) if nested.any?
204
+ result.concat(expand_skills_with_catalog(nested, visited, catalog)) if nested.any?
174
205
 
175
- # Then append this skill
176
206
  result << skill_id
177
207
  end
178
208
 
179
209
  result
180
210
  end
181
211
 
182
-
183
212
  # Extract skills array from metadata.
184
213
  #
185
214
  # @param metadata [PM::Metadata] front matter metadata
@@ -190,7 +219,6 @@ module RobotLab
190
219
  Array(metadata.skills).map(&:to_sym)
191
220
  end
192
221
 
193
-
194
222
  # Accumulate extras from metadata into a hash.
195
223
  # Later calls overwrite earlier values (last-write-wins).
196
224
  #
@@ -209,12 +237,10 @@ module RobotLab
209
237
  extras[:tools] = metadata.tools
210
238
  end
211
239
 
212
- if metadata.respond_to?(:mcp) && metadata.mcp.is_a?(Array)
213
- extras[:mcp] = metadata.mcp.map { |m| m.is_a?(Hash) ? m.transform_keys(&:to_sym) : m }
214
- end
240
+ return unless metadata.respond_to?(:mcp) && metadata.mcp.is_a?(Array)
241
+ extras[:mcp] = metadata.mcp.map { |m| m.is_a?(Hash) ? m.transform_keys(&:to_sym) : m }
215
242
  end
216
243
 
217
-
218
244
  # Apply accumulated extras to the robot, respecting constructor precedence.
219
245
  #
220
246
  # @param extras [Hash] accumulated extras from skills + main template
@@ -231,12 +257,10 @@ module RobotLab
231
257
  @local_tools = resolve_frontmatter_tools(extras[:tools])
232
258
  end
233
259
 
234
- if extras[:mcp] && ToolConfig.none_value?(@mcp_config)
235
- @mcp_config = extras[:mcp]
236
- end
260
+ return unless extras[:mcp] && ToolConfig.none_value?(@mcp_config)
261
+ @mcp_config = extras[:mcp]
237
262
  end
238
263
 
239
-
240
264
  # Extract identity and capability keys from front matter metadata.
241
265
  # Constructor-provided values take precedence over frontmatter.
242
266
  def apply_front_matter_extras(metadata)
@@ -252,12 +276,10 @@ module RobotLab
252
276
  @local_tools = resolve_frontmatter_tools(metadata.tools)
253
277
  end
254
278
 
255
- if metadata.respond_to?(:mcp) && metadata.mcp.is_a?(Array) && ToolConfig.none_value?(@mcp_config)
256
- @mcp_config = metadata.mcp.map { |m| m.is_a?(Hash) ? m.transform_keys(&:to_sym) : m }
257
- end
279
+ return unless metadata.respond_to?(:mcp) && metadata.mcp.is_a?(Array) && ToolConfig.none_value?(@mcp_config)
280
+ @mcp_config = metadata.mcp.map { |m| m.is_a?(Hash) ? m.transform_keys(&:to_sym) : m }
258
281
  end
259
282
 
260
-
261
283
  # Render a parsed template body, returning nil if required params are missing.
262
284
  #
263
285
  # @param parsed [PM::Parsed] the parsed template
@@ -271,7 +293,6 @@ module RobotLab
271
293
  nil
272
294
  end
273
295
 
274
-
275
296
  # Resolve string tool names from frontmatter to Ruby constants.
276
297
  # Tool subclasses are instantiated; instances are used as-is.
277
298
  # Unresolvable names are skipped with a warning.
@@ -294,7 +315,6 @@ module RobotLab
294
315
  end
295
316
  end
296
317
 
297
-
298
318
  def resolve_context(context, network:)
299
319
  case context
300
320
  when Proc then context.call(network: network)