robot_lab 0.0.12 → 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 (243) 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 +108 -0
  12. data/CLAUDE.md +139 -0
  13. data/README.md +91 -45
  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/api/messages/index.md +21 -0
  19. data/docs/examples/index.md +37 -2
  20. data/docs/getting-started/configuration.md +20 -7
  21. data/docs/guides/creating-networks.md +23 -0
  22. data/docs/guides/index.md +16 -16
  23. data/docs/guides/knowledge.md +7 -1
  24. data/docs/guides/observability.md +132 -0
  25. data/docs/index.md +30 -3
  26. data/docs/superpowers/plans/2026-05-06-agentskills.md +1303 -0
  27. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +247 -0
  28. data/examples/.envrc +1 -0
  29. data/examples/01_simple_robot.rb +5 -9
  30. data/examples/02_tools.rb +5 -9
  31. data/examples/03_network.rb +8 -9
  32. data/examples/04_mcp.rb +21 -29
  33. data/examples/05_streaming.rb +12 -18
  34. data/examples/06_prompt_templates.rb +11 -19
  35. data/examples/07_network_memory.rb +16 -31
  36. data/examples/08_llm_config.rb +10 -22
  37. data/examples/09_chaining.rb +16 -27
  38. data/examples/10_memory.rb +12 -28
  39. data/examples/11_network_introspection.rb +15 -29
  40. data/examples/12_message_bus.rb +5 -12
  41. data/examples/13_spawn.rb +5 -10
  42. data/examples/14_rusty_circuit/.envrc +1 -0
  43. data/examples/14_rusty_circuit/comic.rb +2 -0
  44. data/examples/14_rusty_circuit/heckler.rb +1 -1
  45. data/examples/14_rusty_circuit/open_mic.rb +1 -3
  46. data/examples/14_rusty_circuit/scout.rb +2 -0
  47. data/examples/15_memory_network_and_bus/.envrc +1 -0
  48. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +6 -3
  49. data/examples/15_memory_network_and_bus/linux_writer.rb +1 -1
  50. data/examples/15_memory_network_and_bus/output/combined_article.md +6 -6
  51. data/examples/15_memory_network_and_bus/output/final_article.md +6 -8
  52. data/examples/15_memory_network_and_bus/output/linux_draft.md +4 -2
  53. data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
  54. data/examples/15_memory_network_and_bus/output/memory.json +6 -6
  55. data/examples/15_memory_network_and_bus/output/revision_1.md +10 -11
  56. data/examples/15_memory_network_and_bus/output/revision_2.md +6 -8
  57. data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
  58. data/examples/16_writers_room/.envrc +1 -0
  59. data/examples/16_writers_room/writers_room.rb +2 -4
  60. data/examples/17_skills.rb +8 -17
  61. data/examples/18_rails/Gemfile +1 -0
  62. data/examples/18_rails/app/jobs/robot_run_job.rb +15 -75
  63. data/examples/19_token_tracking.rb +9 -15
  64. data/examples/20_circuit_breaker.rb +10 -19
  65. data/examples/21_learning_loop.rb +11 -20
  66. data/examples/22_context_compression.rb +6 -13
  67. data/examples/23_convergence.rb +6 -17
  68. data/examples/24_structured_delegation.rb +11 -15
  69. data/examples/25_history_search.rb +5 -12
  70. data/examples/26_document_store.rb +6 -13
  71. data/examples/27_incident_response/incident_response.rb +4 -5
  72. data/examples/28_mcp_discovery.rb +8 -11
  73. data/examples/29_ractor_tools.rb +4 -9
  74. data/examples/30_ractor_network.rb +10 -19
  75. data/examples/31_launch_assessment.rb +235 -0
  76. data/examples/32_newsletter_reader.rb +188 -0
  77. data/examples/33_stock_generator.rb +80 -0
  78. data/examples/33_stock_predictor.rb +306 -0
  79. data/examples/34_agentskills.rb +72 -0
  80. data/examples/README.md +10 -1
  81. data/examples/common.rb +76 -0
  82. data/examples/ruboruby.md +423 -0
  83. data/examples/temp.md +51 -0
  84. data/lib/robot_lab/agent_skill.rb +63 -0
  85. data/lib/robot_lab/agent_skill_catalog.rb +74 -0
  86. data/lib/robot_lab/ask_user.rb +2 -2
  87. data/lib/robot_lab/bus_poller.rb +12 -5
  88. data/lib/robot_lab/config.rb +1 -12
  89. data/lib/robot_lab/delegation_future.rb +1 -1
  90. data/lib/robot_lab/doom_loop_detector.rb +98 -0
  91. data/lib/robot_lab/history_compressor.rb +4 -10
  92. data/lib/robot_lab/mcp/client.rb +1 -2
  93. data/lib/robot_lab/mcp/connection_poller.rb +3 -3
  94. data/lib/robot_lab/mcp/server.rb +1 -1
  95. data/lib/robot_lab/mcp/server_discovery.rb +0 -2
  96. data/lib/robot_lab/memory.rb +32 -27
  97. data/lib/robot_lab/memory_change.rb +2 -2
  98. data/lib/robot_lab/message.rb +5 -5
  99. data/lib/robot_lab/network.rb +12 -7
  100. data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
  101. data/lib/robot_lab/robot/bus_messaging.rb +9 -27
  102. data/lib/robot_lab/robot/history_search.rb +4 -1
  103. data/lib/robot_lab/robot/mcp_management.rb +5 -11
  104. data/lib/robot_lab/robot/template_rendering.rb +60 -40
  105. data/lib/robot_lab/robot.rb +323 -206
  106. data/lib/robot_lab/robot_result.rb +6 -5
  107. data/lib/robot_lab/run_config.rb +5 -11
  108. data/lib/robot_lab/script_tool.rb +76 -0
  109. data/lib/robot_lab/state_proxy.rb +7 -5
  110. data/lib/robot_lab/tool.rb +3 -3
  111. data/lib/robot_lab/tool_config.rb +1 -1
  112. data/lib/robot_lab/tool_manifest.rb +5 -7
  113. data/lib/robot_lab/user_message.rb +2 -2
  114. data/lib/robot_lab/version.rb +1 -1
  115. data/lib/robot_lab/waiter.rb +1 -1
  116. data/lib/robot_lab.rb +41 -48
  117. data/logfile +8 -0
  118. data/mkdocs.yml +2 -3
  119. data/robot_concurrency.md +38 -0
  120. data/simple_acp_review.md +298 -0
  121. data/site/404.html +2300 -0
  122. data/site/api/core/index.html +2706 -0
  123. data/site/api/core/memory/index.html +3793 -0
  124. data/site/api/core/network/index.html +3500 -0
  125. data/site/api/core/robot/index.html +4566 -0
  126. data/site/api/core/state/index.html +3390 -0
  127. data/site/api/core/tool/index.html +3843 -0
  128. data/site/api/index.html +2635 -0
  129. data/site/api/mcp/client/index.html +3435 -0
  130. data/site/api/mcp/index.html +2783 -0
  131. data/site/api/mcp/server/index.html +3252 -0
  132. data/site/api/mcp/transports/index.html +3352 -0
  133. data/site/api/messages/index.html +2641 -0
  134. data/site/api/messages/text-message/index.html +3087 -0
  135. data/site/api/messages/tool-call-message/index.html +3159 -0
  136. data/site/api/messages/tool-result-message/index.html +3252 -0
  137. data/site/api/messages/user-message/index.html +3212 -0
  138. data/site/api/streaming/context/index.html +3282 -0
  139. data/site/api/streaming/events/index.html +3347 -0
  140. data/site/api/streaming/index.html +2738 -0
  141. data/site/architecture/core-concepts/index.html +3757 -0
  142. data/site/architecture/index.html +2797 -0
  143. data/site/architecture/message-flow/index.html +3238 -0
  144. data/site/architecture/network-orchestration/index.html +3433 -0
  145. data/site/architecture/robot-execution/index.html +3140 -0
  146. data/site/architecture/state-management/index.html +3498 -0
  147. data/site/assets/css/custom.css +56 -0
  148. data/site/assets/images/favicon.png +0 -0
  149. data/site/assets/images/robot_lab.jpg +0 -0
  150. data/site/assets/javascripts/bundle.79ae519e.min.js +16 -0
  151. data/site/assets/javascripts/bundle.79ae519e.min.js.map +7 -0
  152. data/site/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
  153. data/site/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
  154. data/site/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
  155. data/site/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
  156. data/site/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
  157. data/site/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
  158. data/site/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
  159. data/site/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
  160. data/site/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
  161. data/site/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
  162. data/site/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
  163. data/site/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
  164. data/site/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
  165. data/site/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
  166. data/site/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
  167. data/site/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
  168. data/site/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
  169. data/site/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
  170. data/site/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
  171. data/site/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
  172. data/site/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
  173. data/site/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
  174. data/site/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
  175. data/site/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
  176. data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
  177. data/site/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
  178. data/site/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
  179. data/site/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
  180. data/site/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
  181. data/site/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
  182. data/site/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
  183. data/site/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
  184. data/site/assets/javascripts/lunr/tinyseg.js +206 -0
  185. data/site/assets/javascripts/lunr/wordcut.js +6708 -0
  186. data/site/assets/javascripts/workers/search.2c215733.min.js +42 -0
  187. data/site/assets/javascripts/workers/search.2c215733.min.js.map +7 -0
  188. data/site/assets/stylesheets/main.484c7ddc.min.css +1 -0
  189. data/site/assets/stylesheets/main.484c7ddc.min.css.map +1 -0
  190. data/site/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
  191. data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +1 -0
  192. data/site/concepts/index.html +3455 -0
  193. data/site/examples/basic-chat/index.html +2880 -0
  194. data/site/examples/index.html +2907 -0
  195. data/site/examples/mcp-server/index.html +3018 -0
  196. data/site/examples/multi-robot-network/index.html +3131 -0
  197. data/site/examples/rails-application/index.html +3329 -0
  198. data/site/examples/tool-usage/index.html +3085 -0
  199. data/site/getting-started/configuration/index.html +3745 -0
  200. data/site/getting-started/index.html +2572 -0
  201. data/site/getting-started/installation/index.html +2981 -0
  202. data/site/getting-started/quick-start/index.html +2942 -0
  203. data/site/guides/building-robots/index.html +4290 -0
  204. data/site/guides/creating-networks/index.html +3858 -0
  205. data/site/guides/index.html +2586 -0
  206. data/site/guides/mcp-integration/index.html +3581 -0
  207. data/site/guides/memory/index.html +3586 -0
  208. data/site/guides/rails-integration/index.html +4019 -0
  209. data/site/guides/streaming/index.html +3157 -0
  210. data/site/guides/using-tools/index.html +3802 -0
  211. data/site/index.html +2671 -0
  212. data/site/search/search_index.json +1 -0
  213. data/site/sitemap.xml +183 -0
  214. data/site/sitemap.xml.gz +0 -0
  215. data/site/tags.json +1 -0
  216. data/temp.md +6 -0
  217. data/tool_manifest_plan.md +155 -0
  218. metadata +155 -90
  219. data/.github/workflows/deploy-yard-docs.yml +0 -52
  220. data/docs/examples/rails-application.md +0 -419
  221. data/docs/guides/ractor-parallelism.md +0 -364
  222. data/docs/guides/rails-integration.md +0 -642
  223. data/docs/superpowers/plans/2026-04-14-ractor-integration.md +0 -1538
  224. data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +0 -258
  225. data/lib/generators/robot_lab/install_generator.rb +0 -90
  226. data/lib/generators/robot_lab/robot_generator.rb +0 -55
  227. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -42
  228. data/lib/generators/robot_lab/templates/job.rb.tt +0 -92
  229. data/lib/generators/robot_lab/templates/migration.rb.tt +0 -32
  230. data/lib/generators/robot_lab/templates/result_model.rb.tt +0 -52
  231. data/lib/generators/robot_lab/templates/robot.rb.tt +0 -31
  232. data/lib/generators/robot_lab/templates/robot_test.rb.tt +0 -34
  233. data/lib/generators/robot_lab/templates/routing_robot.rb.tt +0 -59
  234. data/lib/generators/robot_lab/templates/thread_model.rb.tt +0 -40
  235. data/lib/robot_lab/document_store.rb +0 -155
  236. data/lib/robot_lab/ractor_boundary.rb +0 -42
  237. data/lib/robot_lab/ractor_job.rb +0 -37
  238. data/lib/robot_lab/ractor_memory_proxy.rb +0 -85
  239. data/lib/robot_lab/ractor_network_scheduler.rb +0 -154
  240. data/lib/robot_lab/ractor_worker_pool.rb +0 -117
  241. data/lib/robot_lab/rails_integration/engine.rb +0 -29
  242. data/lib/robot_lab/rails_integration/railtie.rb +0 -42
  243. data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +0 -72
@@ -13,19 +13,14 @@
13
13
  # Usage:
14
14
  # ruby examples/25_history_search.rb
15
15
 
16
- ENV["ROBOT_LAB_TEMPLATE_PATH"] ||= File.join(__dir__, "prompts")
17
-
18
16
  require "json"
19
- require_relative "../lib/robot_lab"
17
+ require_relative "common"
20
18
 
21
19
  CONVERSATION_TURNS = File.readlines(
22
20
  File.join(__dir__, "25_history_search", "conversation.jsonl"), chomp: true
23
21
  ).map { |line| JSON.parse(line, symbolize_names: true) }.freeze
24
22
 
25
- puts "=" * 60
26
- puts "Example 25: Chat History Search"
27
- puts "=" * 60
28
- puts
23
+ banner "Chat History Search"
29
24
 
30
25
  # ---------------------------------------------------------------------------
31
26
  # Minimal message stub — populates history without LLM calls
@@ -35,7 +30,7 @@ FakeMsg = Struct.new(:role, :content, :tool_calls)
35
30
  # ---------------------------------------------------------------------------
36
31
  # Build a robot and inject the conversation fixture
37
32
  # ---------------------------------------------------------------------------
38
- robot = RobotLab.build(name: "tech_lead", system_prompt: "You are a senior engineering advisor.")
33
+ robot = RobotLab.build(model: LLM[:default].model, name: "tech_lead", system_prompt: "You are a senior engineering advisor.")
39
34
 
40
35
  messages = CONVERSATION_TURNS.map { |t| FakeMsg.new(t[:role], t[:content], nil) }
41
36
  robot.instance_variable_get(:@chat).instance_variable_set(:@messages, messages)
@@ -88,7 +83,7 @@ end
88
83
  # ---------------------------------------------------------------------------
89
84
  # RAG pattern — retrieve the most relevant turns, then inject as context
90
85
  # ---------------------------------------------------------------------------
91
- puts "── RAG pattern: retrieve context, then call LLM ────────────────"
86
+ section "RAG Pattern: Retrieve Context, Then Call LLM"
92
87
  puts "(showing retrieved context — no actual LLM call)"
93
88
  puts
94
89
 
@@ -111,9 +106,7 @@ puts
111
106
  # ---------------------------------------------------------------------------
112
107
  # When to use search_history
113
108
  # ---------------------------------------------------------------------------
114
- puts "=" * 60
115
- puts "When to use search_history"
116
- puts "=" * 60
109
+ section "When to Use search_history"
117
110
  puts <<~'TEXT'
118
111
 
119
112
  Without search_history:
@@ -13,14 +13,10 @@
13
13
  # ruby examples/26_document_store.rb
14
14
  # (Downloads the ~23 MB ONNX model on first run; cached afterwards.)
15
15
 
16
- ENV["ROBOT_LAB_TEMPLATE_PATH"] ||= File.join(__dir__, "prompts")
16
+ require_relative "common"
17
+ require "robot_lab/document_store"
17
18
 
18
- require_relative "../lib/robot_lab"
19
-
20
- puts "=" * 60
21
- puts "Example 26: Embedding-Based Document Store"
22
- puts "=" * 60
23
- puts
19
+ banner "Embedding-Based Document Store"
24
20
  puts "Note: First run downloads the fastembed model (~23 MB, cached)."
25
21
  puts
26
22
 
@@ -97,7 +93,7 @@ end
97
93
  # ---------------------------------------------------------------------------
98
94
  # Delete and verify
99
95
  # ---------------------------------------------------------------------------
100
- puts "── Delete :redis_caching_guide, re-run cache query"
96
+ section "Delete :redis_caching_guide, Re-run Cache Query"
101
97
  store.delete(:redis_caching_guide)
102
98
  results = store.search("Redis evicting keys unexpectedly", limit: 2)
103
99
  puts " Remaining keys: #{store.keys.inspect}"
@@ -107,7 +103,7 @@ puts
107
103
  # ---------------------------------------------------------------------------
108
104
  # Memory integration
109
105
  # ---------------------------------------------------------------------------
110
- puts "── Memory integration"
106
+ section "Memory Integration"
111
107
  memory = RobotLab::Memory.new(enable_cache: false)
112
108
 
113
109
  DOCUMENTS.each { |key, text| memory.store_document(key, text) }
@@ -124,10 +120,7 @@ puts
124
120
  # ---------------------------------------------------------------------------
125
121
  # RAG pattern
126
122
  # ---------------------------------------------------------------------------
127
- puts "=" * 60
128
- puts "RAG Pattern: retrieve relevant docs, then generate with LLM"
129
- puts "=" * 60
130
- puts
123
+ section "RAG Pattern: Retrieve Relevant Docs, Then Generate with LLM"
131
124
 
132
125
  rag_query = "Our Sidekiq jobs exhaust retries and land in the dead queue after a Stripe outage."
133
126
 
@@ -38,11 +38,9 @@
38
38
 
39
39
  ENV["ROBOT_LAB_TEMPLATE_PATH"] ||= File.join(__dir__, "../prompts")
40
40
 
41
- require_relative "../../lib/robot_lab"
41
+ require_relative "../common"
42
42
  require "fileutils"
43
43
 
44
- RubyLLM.configure { |c| c.logger = Logger.new(File::NULL) }
45
-
46
44
  OUTPUT_DIR = File.join(__dir__, "output")
47
45
  FileUtils.mkdir_p(OUTPUT_DIR)
48
46
 
@@ -62,6 +60,7 @@ class SREScout < RobotLab::Robot
62
60
  def initialize(name:, subsystem:, memory_key:, bus: nil)
63
61
  super(
64
62
  name: name,
63
+ model: LLM[:default].model,
65
64
  system_prompt: "You are a senior SRE responding to a production outage. " \
66
65
  "Diagnose one infrastructure layer in 2 sentences: " \
67
66
  "first sentence is root cause, second is customer impact.",
@@ -112,7 +111,7 @@ class WarRoom < RobotLab::Robot
112
111
  attr_reader :updates
113
112
 
114
113
  def initialize(bus:)
115
- super(name: "war_room", system_prompt: "SRE war-room coordinator.", bus: bus)
114
+ super(name: "war_room", model: LLM[:default].model, system_prompt: "SRE war-room coordinator.", bus: bus)
116
115
  @updates = []
117
116
  @delivery_mutex = Mutex.new # only for reading @updates outside Async
118
117
 
@@ -178,7 +177,7 @@ db_scout = SREScout.new(name: "db_scout", subsystem: "database", memory_key
178
177
  net_scout = SREScout.new(name: "net_scout", subsystem: "network", memory_key: :net_finding, bus: bus)
179
178
  app_scout = SREScout.new(name: "app_scout", subsystem: "application", memory_key: :app_finding, bus: bus)
180
179
  war_room = WarRoom.new(bus: bus)
181
- commander = IncidentCommander.new(name: "commander", system_prompt: "SRE incident commander.")
180
+ commander = IncidentCommander.new(name: "commander", model: LLM[:default].model, system_prompt: "SRE incident commander.")
182
181
 
183
182
  # Build the investigation network
184
183
  # poller_group: labels are registered on the network's shared BusPoller.
@@ -12,6 +12,7 @@
12
12
  # == Key config
13
13
  #
14
14
  # robot = RobotLab.build(
15
+ # model: "gpt-5.4",
15
16
  # mcp_discovery: true, # ← enables semantic filtering
16
17
  # mcp: [ ... ] # ← candidate servers, each with :description
17
18
  # )
@@ -29,7 +30,7 @@
29
30
  # Usage:
30
31
  # bundle exec ruby examples/28_mcp_discovery.rb
31
32
 
32
- require_relative "../lib/robot_lab"
33
+ require_relative "common"
33
34
 
34
35
  # Three representative MCP server configurations
35
36
  SERVERS = [
@@ -59,10 +60,8 @@ def show_query(label, query)
59
60
  puts
60
61
  end
61
62
 
62
- puts "=" * 60
63
- puts "Example 28: MCP Server Discovery"
63
+ banner "MCP Server Discovery"
64
64
  puts " Semantic server selection via TF cosine similarity"
65
- puts "=" * 60
66
65
  puts
67
66
  puts "Candidate servers:"
68
67
  SERVERS.each do |s|
@@ -70,14 +69,12 @@ SERVERS.each do |s|
70
69
  end
71
70
  puts
72
71
 
73
- puts "Discovery queries:"
74
- puts "-" * 60
72
+ section "Discovery Queries"
75
73
  show_query("File ops", "read my config file")
76
74
  show_query("Package mgmt", "install imagemagick via homebrew")
77
75
  show_query("Code review", "list open pull requests on my repo")
78
76
 
79
- puts "Fallback cases:"
80
- puts "-" * 60
77
+ section "Fallback Cases"
81
78
 
82
79
  # No description → all servers returned
83
80
  no_desc_servers = SERVERS.map { |s| s.except(:description) }
@@ -93,10 +90,10 @@ result = RobotLab::MCP::ServerDiscovery.select("install imagemagick", from: SERV
93
90
  puts " High threshold : returns all (#{result.size} servers) — no match above 1.0"
94
91
 
95
92
  puts
96
- puts "mcp_discovery: true on a Robot"
97
- puts "-" * 60
93
+ section "mcp_discovery: true on a Robot"
98
94
  puts <<~NOTE
99
95
  RobotLab.build(
96
+ model: "gpt-5.4",
100
97
  name: "assistant",
101
98
  mcp_discovery: true,
102
99
  mcp: [
@@ -109,4 +106,4 @@ puts <<~NOTE
109
106
  # Only the :brew server is connected for this message:
110
107
  robot.run("install imagemagick")
111
108
  NOTE
112
- puts "=" * 60
109
+ hr
@@ -26,9 +26,8 @@
26
26
  # SHA-256 rounds (~320 ms on modern hardware) so the 4-6× speedup is
27
27
  # clearly visible on a 6-core machine.
28
28
 
29
- ENV["ROBOT_LAB_TEMPLATE_PATH"] ||= File.join(__dir__, "prompts")
30
-
31
- require_relative "../lib/robot_lab"
29
+ require_relative "common"
30
+ require "robot_lab/ractor"
32
31
  require "digest"
33
32
 
34
33
  # Always shut down the pool when the process exits.
@@ -115,10 +114,7 @@ end
115
114
  # Demo
116
115
  # =============================================================================
117
116
 
118
- puts "=" * 62
119
- puts "Example 29: Ractor-Safe CPU Tools"
120
- puts "=" * 62
121
- puts
117
+ banner "Ractor-Safe CPU Tools"
122
118
 
123
119
  DIVIDER = ("─" * 54).freeze
124
120
 
@@ -238,6 +234,5 @@ puts " #{DIVIDER}"
238
234
  RobotLab.shutdown_ractor_pool
239
235
  puts " Pool shut down cleanly (poison-pill × #{pool.size} workers)."
240
236
  puts
241
- puts "=" * 62
237
+ hr
242
238
  puts "Example 29 complete."
243
- puts "=" * 62
@@ -30,14 +30,10 @@
30
30
  # bundle exec ruby examples/30_ractor_network.rb # Parts 1 & 2
31
31
  # ANTHROPIC_API_KEY=key ruby examples/30_ractor_network.rb # Parts 1, 2 & 3
32
32
 
33
- ENV["ROBOT_LAB_TEMPLATE_PATH"] ||= File.join(__dir__, "prompts")
33
+ require_relative "common"
34
+ require "robot_lab/ractor"
34
35
 
35
- require_relative "../lib/robot_lab"
36
-
37
- puts "=" * 62
38
- puts "Example 30: Ractor Network Scheduler"
39
- puts "=" * 62
40
- puts
36
+ banner "Ractor Network Scheduler"
41
37
 
42
38
  DIVIDER = ("─" * 54).freeze
43
39
 
@@ -73,8 +69,7 @@ LATENCIES = {
73
69
  #
74
70
  # Speedup ≈ 1.7×
75
71
 
76
- puts "── Part 1: Simulated parallel run (no API key) ───────────"
77
- puts
72
+ section "Part 1: Simulated Parallel Run (no API key)"
78
73
 
79
74
  # SimulatedScheduler overrides execute_spec so that instead of
80
75
  # constructing a real Robot and calling the LLM, it just sleeps for
@@ -140,14 +135,13 @@ puts
140
135
  # Part 2: Network.new(parallel_mode: :ractor) API
141
136
  # =============================================================================
142
137
 
143
- puts "── Part 2: Network.new(parallel_mode: :ractor) ───────────"
144
- puts
138
+ section "Part 2: Network.new(parallel_mode: :ractor)"
145
139
 
146
140
  # When parallel_mode: :ractor is set on a Network, network.run(message:)
147
141
  # routes through RactorNetworkScheduler instead of SimpleFlow::Pipeline.
148
142
  # The default mode is :async (unchanged SimpleFlow behavior).
149
143
 
150
- model = "claude-haiku-4-5-20251001"
144
+ model = LLM[:default].model
151
145
 
152
146
  network = RobotLab::Network.new(name: "research_pipeline", parallel_mode: :ractor) do
153
147
  task :headline_finder, RobotLab.build(name: "headline_finder",
@@ -197,19 +191,17 @@ puts
197
191
  # =============================================================================
198
192
 
199
193
  unless ENV["ANTHROPIC_API_KEY"]
200
- puts "── Part 3: Live LLM run ──────────────────────────────────"
194
+ section "Part 3: Live LLM Run"
201
195
  puts " Set ANTHROPIC_API_KEY to run the real pipeline."
202
196
  puts " Expected behavior: headline_finder, background_brief, and"
203
197
  puts " fact_checker run in parallel; report_writer follows."
204
198
  puts
205
- puts "=" * 62
199
+ hr
206
200
  puts "Example 30 complete."
207
- puts "=" * 62
208
201
  exit 0
209
202
  end
210
203
 
211
- puts "── Part 3: Live LLM run (ANTHROPIC_API_KEY detected) ─────"
212
- puts
204
+ section "Part 3: Live LLM Run (ANTHROPIC_API_KEY detected)"
213
205
 
214
206
  puts " Running 4-robot research pipeline on:"
215
207
  puts " \"#{topic}\""
@@ -251,6 +243,5 @@ rescue RobotLab::Error => e
251
243
  end
252
244
 
253
245
  puts
254
- puts "=" * 62
246
+ hr
255
247
  puts "Example 30 complete."
256
- puts "=" * 62
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 31: Product Launch Assessment — 6 Parallel Analysts, Cap of 4
5
+ #
6
+ # Six specialist robots evaluate a product launch simultaneously.
7
+ # max_concurrent_robots: 4 ensures at most 4 LLM API calls are in-flight
8
+ # at once. Robots 5 and 6 queue behind the Async::Semaphore and start as
9
+ # soon as any of the first 4 finishes — providing natural back-pressure
10
+ # without slowing the pipeline more than necessary.
11
+ #
12
+ # Architecture:
13
+ #
14
+ # ┌──────────────────────────────────────────────────────────────────────┐
15
+ # │ PARALLEL ANALYSIS PHASE (max_concurrent_robots: 4) │
16
+ # │ │
17
+ # │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
18
+ # │ │ Market │ │ Compet. │ │ Tech │ │ Risk │ slots 1-4 │
19
+ # │ │ Analyst │ │ Analyst │ │ Reviewer │ │ Assessor │ │
20
+ # │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
21
+ # │ │ │ │ │ │
22
+ # │ start start start start │
23
+ # │ │
24
+ # │ ┌──────────┐ ┌──────────┐ │
25
+ # │ │Financial │ │ Legal │ queued — start when a slot opens │
26
+ # │ │ Reviewer │ │ Reviewer │ │
27
+ # │ └────┬─────┘ └────┬─────┘ │
28
+ # │ │ │ │
29
+ # │ (deferred) (deferred) │
30
+ # │ │
31
+ # │ ┌─────────────────────────────────────────────────────────────┐ │
32
+ # │ │ SHARED MEMORY │ │
33
+ # │ │ :market :competitive :tech :risk :financial :legal │ │
34
+ # │ └─────────────────────────────────────────────────────────────┘ │
35
+ # │ │ │
36
+ # │ ▼ │
37
+ # │ ┌─────────────────────────────────────────────────────────────┐ │
38
+ # │ │ Launch Director │ │
39
+ # │ │ Blocks on reactive memory until all 6 findings arrive, │ │
40
+ # │ │ then issues a GO / NO-GO recommendation. │ │
41
+ # │ └─────────────────────────────────────────────────────────────┘ │
42
+ # └──────────────────────────────────────────────────────────────────────┘
43
+ #
44
+ # Key config:
45
+ # RunConfig.new(max_concurrent_robots: 4)
46
+ #
47
+ # Usage:
48
+ # ANTHROPIC_API_KEY=your_key ruby examples/31_launch_assessment.rb
49
+
50
+ require_relative "common"
51
+
52
+ # ── AnalystRobot ────────────────────────────────────────────────────────────────
53
+ #
54
+ # Runs its LLM call, writes the verdict to shared memory, and logs a timing line
55
+ # so you can see when the semaphore releases robots 5 and 6.
56
+
57
+ class AnalystRobot < RobotLab::Robot
58
+ attr_reader :memory_key
59
+ attr_writer :shared_memory
60
+
61
+ def initialize(name:, memory_key:, role:)
62
+ super(
63
+ name: name,
64
+ model: LLM[:default].model,
65
+ system_prompt: "You are a #{role}. " \
66
+ "Review the product brief in 2-3 crisp sentences from your area of expertise. " \
67
+ "Close with a one-word verdict: READY or NOT-READY."
68
+ )
69
+ @memory_key = memory_key
70
+ end
71
+
72
+ def call(result)
73
+ brief = extract_brief(result)
74
+ started = Time.now
75
+ puts " [#{name}] started at +#{"%.1f" % (started - $run_start)}s"
76
+
77
+ verdict = run(brief).reply.strip
78
+
79
+ elapsed = "%.1f" % (Time.now - started)
80
+ puts " [#{name}] finished in #{elapsed}s — #{verdict.split.last(2).join(" ")}"
81
+
82
+ if @shared_memory
83
+ @shared_memory.current_writer = name
84
+ @shared_memory.set(@memory_key, verdict)
85
+ end
86
+
87
+ result.with_context(name.to_sym, verdict).continue(verdict)
88
+ end
89
+
90
+ private
91
+
92
+ def extract_brief(result)
93
+ case result.value
94
+ when Hash then result.value[:message].to_s
95
+ when RobotLab::RobotResult then result.value.reply.to_s
96
+ else result.value.to_s
97
+ end
98
+ end
99
+ end
100
+
101
+ # ── LaunchDirector ──────────────────────────────────────────────────────────────
102
+ #
103
+ # Waits for all 6 findings via reactive memory, then issues the final call.
104
+ # SimpleFlow guarantees the six analyst tasks are done before this task runs,
105
+ # so the memory.get is effectively a non-blocking read by the time we arrive here.
106
+
107
+ class LaunchDirector < RobotLab::Robot
108
+ attr_writer :shared_memory
109
+
110
+ FINDING_KEYS = %i[market competitive tech risk financial legal].freeze
111
+
112
+ def call(result)
113
+ puts " [#{name}] reading all findings from shared memory..."
114
+ findings = @shared_memory.get(*FINDING_KEYS, wait: 120)
115
+
116
+ if findings.values.any? { |v| v == :timeout }
117
+ timed_out = findings.select { |_, v| v == :timeout }.keys
118
+ puts " [#{name}] WARNING: timed out waiting for: #{timed_out.join(", ")}"
119
+ end
120
+
121
+ prompt = <<~PROMPT
122
+ Six specialist analysts have reviewed our product launch readiness.
123
+ Based on their findings, issue a final GO or NO-GO recommendation
124
+ in 3-5 sentences. Be direct and specific about the key deciding factors.
125
+
126
+ Market analysis: #{findings[:market] || "(not received)"}
127
+ Competitive analysis: #{findings[:competitive] || "(not received)"}
128
+ Technical review: #{findings[:tech] || "(not received)"}
129
+ Risk assessment: #{findings[:risk] || "(not received)"}
130
+ Financial review: #{findings[:financial] || "(not received)"}
131
+ Legal review: #{findings[:legal] || "(not received)"}
132
+
133
+ Begin your response with "GO -" or "NO-GO -".
134
+ PROMPT
135
+
136
+ recommendation = run(prompt).reply.strip
137
+ @shared_memory.set(:recommendation, recommendation)
138
+ puts " [#{name}] recommendation ready"
139
+
140
+ result.with_context(:recommendation, recommendation).continue(recommendation)
141
+ end
142
+ end
143
+
144
+ # ── Product Brief ───────────────────────────────────────────────────────────────
145
+
146
+ PRODUCT_BRIEF = <<~BRIEF
147
+ Product: "Orion" — an AI-powered project management tool that auto-generates
148
+ sprint plans from Jira backlogs, detects scope creep in real-time, and integrates
149
+ with GitHub and Slack via webhooks. SaaS pricing: $25/seat/month, 14-day free trial.
150
+ Target: mid-size engineering teams (20-200 developers). Launch date: 6 weeks out.
151
+ Beta: 12 paying customers, 94% satisfaction, 0 critical bugs open. SOC 2 Type I
152
+ certification in progress, expected within 30 days.
153
+ BRIEF
154
+
155
+ # ── Build the six analysts ───────────────────────────────────────────────────────
156
+
157
+ ANALYSTS = [
158
+ { name: "market_analyst", key: :market, role: "market opportunity analyst" },
159
+ { name: "competitive_analyst", key: :competitive, role: "competitive intelligence analyst" },
160
+ { name: "tech_reviewer", key: :tech, role: "technical readiness and quality reviewer" },
161
+ { name: "risk_assessor", key: :risk, role: "product risk assessment specialist" },
162
+ { name: "financial_reviewer", key: :financial, role: "financial viability and pricing analyst" },
163
+ { name: "legal_reviewer", key: :legal, role: "legal, compliance, and IP reviewer" },
164
+ ].freeze
165
+
166
+ analyst_robots = ANALYSTS.map do |spec|
167
+ AnalystRobot.new(name: spec[:name], memory_key: spec[:key], role: spec[:role])
168
+ end
169
+
170
+ director = LaunchDirector.new(
171
+ name: "launch_director",
172
+ model: LLM[:default].model,
173
+ system_prompt: "You are the VP of Product making the final launch call."
174
+ )
175
+
176
+ # ── Network — note the concurrency cap ──────────────────────────────────────────
177
+
178
+ config = RobotLab::RunConfig.new(max_concurrent_robots: 4)
179
+
180
+ analyst_names = analyst_robots.map { |r| r.name.to_sym }
181
+
182
+ network = RobotLab.create_network(name: "launch_assessment", config: config) do
183
+ analyst_robots.each do |robot|
184
+ task robot.name.to_sym, robot, depends_on: :none
185
+ end
186
+
187
+ task :launch_director, director, depends_on: analyst_names
188
+ end
189
+
190
+ # Assign shared memory so each robot can write to it directly
191
+ shared_memory = network.memory
192
+ (analyst_robots + [director]).each { |r| r.shared_memory = shared_memory }
193
+
194
+ # Subscribe for a memory-level audit trail
195
+ analyst_robots.each do |robot|
196
+ network.memory.subscribe(robot.memory_key) do |change|
197
+ puts " [memory] :#{change.key} written by #{change.writer}"
198
+ end
199
+ end
200
+
201
+ # ── Run ─────────────────────────────────────────────────────────────────────────
202
+
203
+ banner "Product Launch Assessment"
204
+ puts " 6 specialist analysts in parallel, max_concurrent_robots: 4"
205
+ puts
206
+ puts "Pipeline:"
207
+ puts network.visualize
208
+ puts
209
+ puts "Concurrency config: #{config.inspect}"
210
+ puts
211
+ puts "Product brief:"
212
+ puts PRODUCT_BRIEF.strip.gsub(/^/, " ")
213
+ puts
214
+ section "Running — Analysts 5 and 6 Queue Until a Semaphore Slot Opens"
215
+
216
+ $run_start = Time.now
217
+ result = network.run(message: PRODUCT_BRIEF)
218
+ elapsed = "%.1f" % (Time.now - $run_start)
219
+
220
+ puts
221
+ hr
222
+ puts "All analysts complete. Total wall time: #{elapsed}s"
223
+ hr
224
+
225
+ section "Launch Director Recommendation"
226
+ puts network.memory[:recommendation]
227
+ puts
228
+ section "Individual Analyst Verdicts"
229
+ analyst_robots.each do |robot|
230
+ label = robot.name.gsub("_", " ").upcase
231
+ finding = network.memory.get(robot.memory_key).to_s
232
+ puts "#{label}"
233
+ puts finding
234
+ puts
235
+ end