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
@@ -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
@@ -47,11 +47,7 @@
47
47
  # Usage:
48
48
  # ANTHROPIC_API_KEY=your_key ruby examples/31_launch_assessment.rb
49
49
 
50
- ENV["ROBOT_LAB_TEMPLATE_PATH"] ||= File.join(__dir__, "prompts")
51
-
52
- require_relative "../lib/robot_lab"
53
-
54
- RubyLLM.configure { |c| c.logger = Logger.new(File::NULL) }
50
+ require_relative "common"
55
51
 
56
52
  # ── AnalystRobot ────────────────────────────────────────────────────────────────
57
53
  #
@@ -65,6 +61,7 @@ class AnalystRobot < RobotLab::Robot
65
61
  def initialize(name:, memory_key:, role:)
66
62
  super(
67
63
  name: name,
64
+ model: LLM[:default].model,
68
65
  system_prompt: "You are a #{role}. " \
69
66
  "Review the product brief in 2-3 crisp sentences from your area of expertise. " \
70
67
  "Close with a one-word verdict: READY or NOT-READY."
@@ -172,6 +169,7 @@ end
172
169
 
173
170
  director = LaunchDirector.new(
174
171
  name: "launch_director",
172
+ model: LLM[:default].model,
175
173
  system_prompt: "You are the VP of Product making the final launch call."
176
174
  )
177
175
 
@@ -202,10 +200,8 @@ end
202
200
 
203
201
  # ── Run ─────────────────────────────────────────────────────────────────────────
204
202
 
205
- puts "=" * 68
206
- puts "Example 31: Product Launch Assessment"
203
+ banner "Product Launch Assessment"
207
204
  puts " 6 specialist analysts in parallel, max_concurrent_robots: 4"
208
- puts "=" * 68
209
205
  puts
210
206
  puts "Pipeline:"
211
207
  puts network.visualize
@@ -215,30 +211,21 @@ puts
215
211
  puts "Product brief:"
216
212
  puts PRODUCT_BRIEF.strip.gsub(/^/, " ")
217
213
  puts
218
- puts "-" * 68
219
- puts "Running — analysts 5 and 6 queue until a semaphore slot opens..."
220
- puts "-" * 68
221
- puts
214
+ section "Running Analysts 5 and 6 Queue Until a Semaphore Slot Opens"
222
215
 
223
216
  $run_start = Time.now
224
217
  result = network.run(message: PRODUCT_BRIEF)
225
218
  elapsed = "%.1f" % (Time.now - $run_start)
226
219
 
227
220
  puts
228
- puts "-" * 68
221
+ hr
229
222
  puts "All analysts complete. Total wall time: #{elapsed}s"
230
- puts "-" * 68
231
- puts
232
- puts "=" * 68
233
- puts "LAUNCH DIRECTOR RECOMMENDATION"
234
- puts "=" * 68
235
- puts
223
+ hr
224
+
225
+ section "Launch Director Recommendation"
236
226
  puts network.memory[:recommendation]
237
227
  puts
238
- puts "=" * 68
239
- puts "INDIVIDUAL ANALYST VERDICTS"
240
- puts "=" * 68
241
- puts
228
+ section "Individual Analyst Verdicts"
242
229
  analyst_robots.each do |robot|
243
230
  label = robot.name.gsub("_", " ").upcase
244
231
  finding = network.memory.get(robot.memory_key).to_s
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 32: Newsletter Issue Retriever
5
+ #
6
+ # Fetches all unprocessed issues from multiple Ruby newsletters via RSS
7
+ # and saves each as a Markdown file in the Obsidian Clippings folder.
8
+ #
9
+ # - Processes oldest unprocessed issue first (across all feeds)
10
+ # - Filename: <newsletter-name>_YYYYMMDD.md
11
+ # - Includes YAML frontmatter with source URL (compatible with Clippings workflow)
12
+ # - Tracks processed issue URLs in ~/.robot_lab/newsletter_processed.yaml
13
+ #
14
+ # Usage:
15
+ # ruby examples/32_newsletter_reader.rb
16
+
17
+ require "net/http"
18
+ require "open3"
19
+ require "time"
20
+ require "uri"
21
+ require "yaml"
22
+ require "fileutils"
23
+ require "rexml/document"
24
+ require "rexml/xpath"
25
+
26
+ NEWSLETTER_RSS_URLS = [
27
+ "https://rss.beehiiv.com/feeds/MTJunJRFxo.xml", # Ruby AI News
28
+ "https://cprss.s3.amazonaws.com/rubyweekly.com.xml" # Ruby Weekly
29
+ ].freeze
30
+
31
+ CLIPPINGS_DIR = File.expand_path("/Users/dewayne/Documents/obsidian_order_intelligence/PKM/Clippings")
32
+ PROCESSED_STATE_FILE = File.join(Dir.home, ".robot_lab", "newsletter_processed.yaml")
33
+
34
+ # Tracks which newsletter issue URLs have been saved.
35
+ class ProcessedIssues
36
+ def initialize(path: PROCESSED_STATE_FILE)
37
+ @path = path
38
+ @urls = load_urls
39
+ end
40
+
41
+ def processed?(url)
42
+ @urls.include?(url)
43
+ end
44
+
45
+ def mark_processed(url)
46
+ @urls << url
47
+ FileUtils.mkdir_p(File.dirname(@path))
48
+ File.write(@path, YAML.dump(@urls.uniq))
49
+ end
50
+
51
+ def count = @urls.size
52
+
53
+ private
54
+
55
+ def load_urls
56
+ return [] unless File.exist?(@path)
57
+ Array(YAML.safe_load(File.read(@path)) || [])
58
+ end
59
+ end
60
+
61
+ # Fetches a single RSS feed URL and returns its items (unsorted).
62
+ # Each item: { title:, url:, pub_date:, published_at:, html:, channel_name: }
63
+ def fetch_rss_items(url)
64
+ uri = URI(url)
65
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |h| h.get(uri.request_uri) }
66
+ raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
67
+
68
+ doc = REXML::Document.new(response.body)
69
+ ns = { "content" => "http://purl.org/rss/1.0/modules/content/" }
70
+
71
+ channel_title = REXML::XPath.first(doc, "//channel/title")&.text&.strip || "newsletter"
72
+
73
+ REXML::XPath.match(doc, "//channel/item").filter_map do |item|
74
+ item_url = REXML::XPath.first(item, "link")&.text&.strip
75
+ title = REXML::XPath.first(item, "title")&.text&.strip
76
+ pub_date = REXML::XPath.first(item, "pubDate")&.text&.strip
77
+ html = REXML::XPath.first(item, "content:encoded", ns)&.text ||
78
+ REXML::XPath.first(item, "description")&.text || ""
79
+
80
+ next unless item_url && title && pub_date
81
+
82
+ {
83
+ title: title,
84
+ url: item_url,
85
+ pub_date: pub_date,
86
+ published_at: Time.parse(pub_date),
87
+ html: html,
88
+ channel_name: channel_title
89
+ }
90
+ end
91
+ end
92
+
93
+ # Fetches all configured RSS feeds and returns items sorted oldest-first.
94
+ def fetch_all_rss_items
95
+ NEWSLETTER_RSS_URLS.flat_map do |feed_url|
96
+ print " #{feed_url}... "
97
+ items = fetch_rss_items(feed_url)
98
+ puts "#{items.size} issues"
99
+ items
100
+ end.sort_by { |item| item[:published_at] }
101
+ end
102
+
103
+ # Converts HTML to Markdown via html2markdown CLI, stripping UTM params.
104
+ def html_to_markdown(html)
105
+ md, = Open3.capture3(
106
+ "html2markdown",
107
+ stdin_data: html
108
+ )
109
+ md.gsub(/\]\(([^)]+)\)/) do
110
+ url = $1
111
+ if url.include?("?")
112
+ base, query = url.split("?", 2)
113
+ kept = query.split("&").reject { |p| p.start_with?("utm_") }
114
+ "](#{kept.empty? ? base : "#{base}?#{kept.join("&")}"})"
115
+ else
116
+ "](#{url})"
117
+ end
118
+ end
119
+ end
120
+
121
+ # Builds the output filename: <newsletter-name>_YYYYMMDD.md
122
+ def output_filename(channel_name, published_at)
123
+ safe_name = channel_name.downcase.gsub(/[^a-z0-9]+/, "_").delete_suffix("_")
124
+ date_str = published_at.strftime("%Y%m%d")
125
+ "#{safe_name}_#{date_str}.md"
126
+ end
127
+
128
+ # Wraps markdown content in YAML frontmatter compatible with the Clippings workflow.
129
+ def wrap_with_frontmatter(title:, url:, pub_date:, body:)
130
+ date = Time.parse(pub_date).strftime("%Y-%m-%d")
131
+ <<~MD
132
+ ---
133
+ source: #{url}
134
+ title: "#{title.gsub('"', '\\"')}"
135
+ date: #{date}
136
+ ---
137
+
138
+ # #{title}
139
+
140
+ #{body.strip}
141
+ MD
142
+ end
143
+
144
+ # ── Main ──────────────────────────────────────────────────────────────────────
145
+
146
+ puts "=" * 60
147
+ puts "Example 32: Newsletter Issue Retriever"
148
+ puts "=" * 60
149
+ puts
150
+
151
+ FileUtils.mkdir_p(CLIPPINGS_DIR)
152
+ state = ProcessedIssues.new
153
+
154
+ puts "Fetching #{NEWSLETTER_RSS_URLS.size} RSS feeds..."
155
+ all_items = fetch_all_rss_items
156
+ pending = all_items.reject { |item| state.processed?(item[:url]) }
157
+ puts "#{all_items.size} total issues found across all feeds."
158
+
159
+ if pending.empty?
160
+ puts "All issues already saved. Nothing to do."
161
+ exit
162
+ end
163
+
164
+ puts "#{pending.size} unprocessed (#{state.count} already done). Saving oldest-first."
165
+ puts
166
+
167
+ pending.each_with_index do |item, idx|
168
+ filename = output_filename(item[:channel_name], item[:published_at])
169
+ filepath = File.join(CLIPPINGS_DIR, filename)
170
+
171
+ print "[#{idx + 1}/#{pending.size}] #{item[:title]} (#{item[:published_at].strftime("%Y-%m-%d")})... "
172
+
173
+ body = html_to_markdown(item[:html])
174
+ content = wrap_with_frontmatter(
175
+ title: item[:title],
176
+ url: item[:url],
177
+ pub_date: item[:pub_date],
178
+ body: body
179
+ )
180
+
181
+ File.write(filepath, content)
182
+ state.mark_processed(item[:url])
183
+
184
+ puts "saved → #{filename}"
185
+ end
186
+
187
+ puts
188
+ puts "Done. #{pending.size} issue(s) saved to #{CLIPPINGS_DIR}"
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 33: XYZZY Stock Price Generator
5
+ #
6
+ # Publishes fake streaming prices for ticker XYZZY to a Redis channel
7
+ # using Geometric Brownian Motion with occasional volatility regime shifts.
8
+ #
9
+ # Prerequisites:
10
+ # gem install redis
11
+ # Redis server running on localhost:6379
12
+ #
13
+ # Usage:
14
+ # ruby examples/33_stock_generator.rb
15
+
16
+ require "redis"
17
+ require "json"
18
+ require "time"
19
+
20
+ CHANNEL = "stock:xyzzy"
21
+ START_PRICE = 100.0
22
+ BASE_VOL = 0.008 # baseline volatility per tick (~0.8%)
23
+ DRIFT = 0.0001 # slight upward drift per tick
24
+
25
+ # Box-Muller transform — standard normal sample
26
+ def randn
27
+ Math.sqrt(-2.0 * Math.log(rand)) * Math.cos(2.0 * Math::PI * rand)
28
+ end
29
+
30
+ # Occasionally shift the volatility regime to create interesting price dynamics
31
+ def current_volatility(tick)
32
+ case tick % 60
33
+ when 0..10 then BASE_VOL * 2.0 # high volatility burst
34
+ when 30..35 then BASE_VOL * 0.4 # low volatility squeeze
35
+ else BASE_VOL
36
+ end
37
+ end
38
+
39
+ # ── Main ──────────────────────────────────────────────────────────────────────
40
+
41
+ redis = Redis.new
42
+ price = START_PRICE
43
+ tick = 0
44
+
45
+ trap("INT") { puts "\nGenerator stopped."; exit }
46
+
47
+ puts "=" * 50
48
+ puts "XYZZY Stock Generator"
49
+ puts "=" * 50
50
+ puts "Channel : #{CHANNEL}"
51
+ puts "Interval: 5 seconds per tick"
52
+ puts "Model : Geometric Brownian Motion"
53
+ puts "Press Ctrl-C to stop."
54
+ puts "-" * 50
55
+
56
+ loop do
57
+ tick += 1
58
+
59
+ vol = current_volatility(tick)
60
+ price = (price * Math.exp((DRIFT - 0.5 * vol**2) + vol * randn)).round(2)
61
+ price = [price, 1.0].max # floor at $1.00
62
+
63
+ regime = case vol
64
+ when BASE_VOL * 2.0 then " [HIGH VOL]"
65
+ when BASE_VOL * 0.4 then " [low vol]"
66
+ else ""
67
+ end
68
+
69
+ payload = JSON.generate(
70
+ ticker: "XYZZY",
71
+ price: price,
72
+ tick: tick,
73
+ timestamp: Time.now.iso8601
74
+ )
75
+
76
+ redis.publish(CHANNEL, payload)
77
+ puts "Tick %5d $%8.2f vol=%.3f%s" % [tick, price, vol, regime]
78
+
79
+ sleep 5
80
+ end