robot_lab 0.3.0 → 0.3.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 (256) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +1 -1
  4. data/lib/robot_lab/mcp/transports/streamable_http.rb +1 -1
  5. data/lib/robot_lab/version.rb +1 -1
  6. metadata +3 -253
  7. data/.architecture/AGENTS.md +0 -32
  8. data/.architecture/config.yml +0 -8
  9. data/.architecture/members.yml +0 -60
  10. data/.architecture/reviews/feature-free-will.md +0 -490
  11. data/.architecture/reviews/overall-codebase.md +0 -427
  12. data/.claude/settings.local.json +0 -57
  13. data/.codex/config.toml +0 -2
  14. data/.envrc +0 -6
  15. data/.github/workflows/deploy-github-pages.yml +0 -52
  16. data/.irbrc +0 -6
  17. data/.loki +0 -65
  18. data/.quality/reek_baseline.txt +0 -43
  19. data/.rubocop.yml +0 -10
  20. data/Archspec.rb +0 -44
  21. data/CLAUDE.md +0 -141
  22. data/COMMITS.md +0 -196
  23. data/Rakefile +0 -193
  24. data/_typos.toml +0 -21
  25. data/agent2agent_review.md +0 -192
  26. data/agentf_improvements.md +0 -253
  27. data/agents.md +0 -14
  28. data/docs/api/core/index.md +0 -147
  29. data/docs/api/core/memory.md +0 -578
  30. data/docs/api/core/network.md +0 -553
  31. data/docs/api/core/result.md +0 -211
  32. data/docs/api/core/robot.md +0 -1667
  33. data/docs/api/core/state.md +0 -145
  34. data/docs/api/core/tool.md +0 -520
  35. data/docs/api/errors.md +0 -323
  36. data/docs/api/hooks.md +0 -469
  37. data/docs/api/index.md +0 -137
  38. data/docs/api/mcp/client.md +0 -387
  39. data/docs/api/mcp/index.md +0 -267
  40. data/docs/api/mcp/server.md +0 -252
  41. data/docs/api/mcp/transports.md +0 -339
  42. data/docs/api/messages/index.md +0 -126
  43. data/docs/api/messages/text-message.md +0 -170
  44. data/docs/api/messages/tool-call-message.md +0 -206
  45. data/docs/api/messages/tool-result-message.md +0 -242
  46. data/docs/api/messages/user-message.md +0 -232
  47. data/docs/api/skills.md +0 -423
  48. data/docs/api/streaming/context.md +0 -280
  49. data/docs/api/streaming/events.md +0 -185
  50. data/docs/api/streaming/index.md +0 -156
  51. data/docs/api/support.md +0 -846
  52. data/docs/architecture/core-concepts.md +0 -575
  53. data/docs/architecture/index.md +0 -252
  54. data/docs/architecture/message-flow.md +0 -366
  55. data/docs/architecture/network-orchestration.md +0 -478
  56. data/docs/architecture/robot-execution.md +0 -396
  57. data/docs/architecture/state-management.md +0 -410
  58. data/docs/assets/css/custom.css +0 -56
  59. data/docs/assets/images/robot_lab.jpg +0 -0
  60. data/docs/concepts.md +0 -641
  61. data/docs/examples/basic-chat.md +0 -258
  62. data/docs/examples/index.md +0 -425
  63. data/docs/examples/mcp-server.md +0 -403
  64. data/docs/examples/multi-robot-network.md +0 -476
  65. data/docs/examples/tool-usage.md +0 -468
  66. data/docs/getting-started/configuration.md +0 -645
  67. data/docs/getting-started/index.md +0 -56
  68. data/docs/getting-started/installation.md +0 -197
  69. data/docs/getting-started/quick-start.md +0 -293
  70. data/docs/guides/building-robots.md +0 -1187
  71. data/docs/guides/creating-networks.md +0 -736
  72. data/docs/guides/hooks.md +0 -1206
  73. data/docs/guides/index.md +0 -78
  74. data/docs/guides/knowledge.md +0 -219
  75. data/docs/guides/mcp-integration.md +0 -740
  76. data/docs/guides/memory.md +0 -458
  77. data/docs/guides/observability.md +0 -733
  78. data/docs/guides/streaming.md +0 -323
  79. data/docs/guides/using-tools.md +0 -780
  80. data/docs/index.md +0 -280
  81. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  82. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  83. data/examples/.envrc +0 -3
  84. data/examples/01_simple_robot.rb +0 -31
  85. data/examples/02_tools.rb +0 -109
  86. data/examples/03_network.rb +0 -118
  87. data/examples/04_mcp.rb +0 -217
  88. data/examples/05_streaming.rb +0 -137
  89. data/examples/06_prompt_templates.rb +0 -325
  90. data/examples/07_network_memory.rb +0 -313
  91. data/examples/08_llm_config.rb +0 -162
  92. data/examples/09_chaining.rb +0 -251
  93. data/examples/10_memory.rb +0 -315
  94. data/examples/11_network_introspection.rb +0 -239
  95. data/examples/12_message_bus.rb +0 -67
  96. data/examples/13_spawn.rb +0 -85
  97. data/examples/14_rusty_circuit/.envrc +0 -1
  98. data/examples/14_rusty_circuit/.gitignore +0 -1
  99. data/examples/14_rusty_circuit/README.md +0 -1
  100. data/examples/14_rusty_circuit/comic.rb +0 -149
  101. data/examples/14_rusty_circuit/display.rb +0 -217
  102. data/examples/14_rusty_circuit/heckler.rb +0 -65
  103. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  104. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  105. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  106. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  107. data/examples/14_rusty_circuit/scout.rb +0 -165
  108. data/examples/14_rusty_circuit/show.log +0 -234
  109. data/examples/15_memory_network_and_bus/.envrc +0 -1
  110. data/examples/15_memory_network_and_bus/README.md +0 -66
  111. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  112. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  113. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  114. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  115. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  116. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
  117. data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
  118. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  119. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  120. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  121. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
  122. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
  123. data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
  124. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  125. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  126. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  127. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  128. data/examples/16_writers_room/.envrc +0 -1
  129. data/examples/16_writers_room/display.rb +0 -158
  130. data/examples/16_writers_room/output/.gitignore +0 -4
  131. data/examples/16_writers_room/output/README.md +0 -69
  132. data/examples/16_writers_room/output/opus_001.md +0 -263
  133. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  134. data/examples/16_writers_room/output/opus_002.md +0 -245
  135. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  136. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  137. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  138. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  139. data/examples/16_writers_room/prompts/writer.md +0 -37
  140. data/examples/16_writers_room/room.rb +0 -186
  141. data/examples/16_writers_room/tools.rb +0 -173
  142. data/examples/16_writers_room/writer.rb +0 -121
  143. data/examples/16_writers_room/writers_room.rb +0 -256
  144. data/examples/17_skills.rb +0 -234
  145. data/examples/18_rails/.envrc +0 -3
  146. data/examples/18_rails/.gitignore +0 -5
  147. data/examples/18_rails/Gemfile +0 -11
  148. data/examples/18_rails/README.md +0 -67
  149. data/examples/18_rails/Rakefile +0 -4
  150. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  151. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  152. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  153. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  154. data/examples/18_rails/app/models/application_record.rb +0 -5
  155. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  156. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  157. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  158. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  159. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  160. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  161. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  162. data/examples/18_rails/bin/dev +0 -7
  163. data/examples/18_rails/bin/rails +0 -6
  164. data/examples/18_rails/bin/setup +0 -15
  165. data/examples/18_rails/config/application.rb +0 -33
  166. data/examples/18_rails/config/cable.yml +0 -2
  167. data/examples/18_rails/config/database.yml +0 -5
  168. data/examples/18_rails/config/environment.rb +0 -4
  169. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  170. data/examples/18_rails/config/routes.rb +0 -6
  171. data/examples/18_rails/config.ru +0 -4
  172. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  173. data/examples/19_token_tracking.rb +0 -138
  174. data/examples/20_circuit_breaker.rb +0 -147
  175. data/examples/21_learning_loop.rb +0 -181
  176. data/examples/22_context_compression.rb +0 -172
  177. data/examples/23_convergence.rb +0 -133
  178. data/examples/24_structured_delegation.rb +0 -151
  179. data/examples/25_history_search/conversation.jsonl +0 -30
  180. data/examples/25_history_search.rb +0 -133
  181. data/examples/26_document_store/api_versioning_adr.md +0 -52
  182. data/examples/26_document_store/incident_postmortem.md +0 -46
  183. data/examples/26_document_store/postgres_runbook.md +0 -49
  184. data/examples/26_document_store/redis_caching_guide.md +0 -48
  185. data/examples/26_document_store/sidekiq_guide.md +0 -51
  186. data/examples/26_document_store.rb +0 -140
  187. data/examples/27_incident_response/README.md +0 -65
  188. data/examples/27_incident_response/incident_response.rb +0 -261
  189. data/examples/28_mcp_discovery.rb +0 -113
  190. data/examples/29_ractor_tools.rb +0 -240
  191. data/examples/30_ractor_network.rb +0 -252
  192. data/examples/31_launch_assessment.rb +0 -246
  193. data/examples/32_newsletter_reader.rb +0 -193
  194. data/examples/33_stock_generator.rb +0 -80
  195. data/examples/33_stock_predictor.rb +0 -327
  196. data/examples/34_agentskills.rb +0 -76
  197. data/examples/35_hooks.rb +0 -266
  198. data/examples/README.md +0 -550
  199. data/examples/common.rb +0 -180
  200. data/examples/prompts/assistant.md +0 -5
  201. data/examples/prompts/audit_trail.md +0 -8
  202. data/examples/prompts/billing.md +0 -10
  203. data/examples/prompts/classifier.md +0 -7
  204. data/examples/prompts/comedian.md +0 -6
  205. data/examples/prompts/comedy_critic.md +0 -10
  206. data/examples/prompts/configurable.md +0 -9
  207. data/examples/prompts/dispatcher.md +0 -12
  208. data/examples/prompts/entity_extractor.md +0 -14
  209. data/examples/prompts/escalation.md +0 -42
  210. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  211. data/examples/prompts/frontmatter_named_test.md +0 -5
  212. data/examples/prompts/frontmatter_tools_test.md +0 -6
  213. data/examples/prompts/general.md +0 -7
  214. data/examples/prompts/github_assistant.md +0 -14
  215. data/examples/prompts/helper.md +0 -4
  216. data/examples/prompts/incident_responder.md +0 -18
  217. data/examples/prompts/keyword_extractor.md +0 -11
  218. data/examples/prompts/llm_config_demo.md +0 -20
  219. data/examples/prompts/order_support.md +0 -35
  220. data/examples/prompts/os_advocate.md +0 -13
  221. data/examples/prompts/os_chief.md +0 -13
  222. data/examples/prompts/os_editor.md +0 -13
  223. data/examples/prompts/parameterized_main_test.md +0 -6
  224. data/examples/prompts/pii_redactor.md +0 -7
  225. data/examples/prompts/product_support.md +0 -37
  226. data/examples/prompts/runbook_protocol.md +0 -12
  227. data/examples/prompts/sentiment_analyzer.md +0 -12
  228. data/examples/prompts/skill_a_test.md +0 -4
  229. data/examples/prompts/skill_b_test.md +0 -4
  230. data/examples/prompts/skill_config_test.md +0 -5
  231. data/examples/prompts/skill_cycle_a_test.md +0 -6
  232. data/examples/prompts/skill_cycle_b_test.md +0 -6
  233. data/examples/prompts/skill_description_test.md +0 -4
  234. data/examples/prompts/skill_leaf_test.md +0 -4
  235. data/examples/prompts/skill_nested_test.md +0 -6
  236. data/examples/prompts/skill_refs_main_test.md +0 -6
  237. data/examples/prompts/skill_self_ref_test.md +0 -6
  238. data/examples/prompts/skill_with_mcp_test.md +0 -9
  239. data/examples/prompts/skill_with_params_test.md +0 -6
  240. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  241. data/examples/prompts/skill_with_tools_test.md +0 -6
  242. data/examples/prompts/sre_compliance.md +0 -10
  243. data/examples/prompts/structured_output.md +0 -7
  244. data/examples/prompts/synthesizer.md +0 -17
  245. data/examples/prompts/technical.md +0 -10
  246. data/examples/prompts/template_with_skills_test.md +0 -6
  247. data/examples/prompts/triage.md +0 -22
  248. data/examples/ruboruby.md +0 -423
  249. data/examples/run_all.rb +0 -60
  250. data/examples/xyzzy.rb +0 -97
  251. data/logfile +0 -8
  252. data/mkdocs.yml +0 -209
  253. data/robot_concurrency.md +0 -38
  254. data/simple_acp_review.md +0 -298
  255. data/temp.md +0 -6
  256. data/tool_manifest_plan.md +0 -155
@@ -1,252 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Example 30: Ractor Network Scheduler
5
- #
6
- # Demonstrates Track 2 of RobotLab's Ractor parallelism: running a
7
- # multi-robot pipeline under RactorNetworkScheduler, which dispatches
8
- # independent tasks in parallel waves and respects depends_on ordering.
9
- #
10
- # Concepts covered:
11
- # - Network.new(parallel_mode: :ractor) — opt the network into Ractor dispatch
12
- # - Dependency waves — independent tasks run concurrently,
13
- # dependent tasks wait for their wave
14
- # - RobotSpec — frozen, Ractor-shareable robot descriptor
15
- # - RactorNetworkScheduler — direct use for testing / custom wiring
16
- # - Result Hash — run_pipeline returns { name => result }
17
- #
18
- # Part 1 — simulated scheduler, no LLM traffic.
19
- # Overrides execute_spec with a sleep-based stub so the wave ordering and
20
- # timing characteristics are visible without real LLM calls.
21
- #
22
- # Part 2 — Network.new(parallel_mode: :ractor) API walkthrough.
23
- # Shows how to configure the network and inspects the dependency graph.
24
- # No run() call is made, so no LLM traffic either.
25
- #
26
- # Part 3 — live LLM run (opt-in via RUN_LIVE).
27
- # Expected to fail against ruby_llm today; the rescue clauses explain why.
28
- #
29
- # Usage:
30
- # bundle exec ruby examples/30_ractor_network.rb # Parts 1 & 2
31
- # RUN_LIVE=1 bundle exec ruby examples/30_ractor_network.rb # Parts 1, 2 & 3
32
-
33
- require_relative "common"
34
- require "robot_lab/ractor"
35
-
36
- banner "Ractor Network Scheduler"
37
-
38
- DIVIDER = ("─" * 54).freeze
39
-
40
- # =============================================================================
41
- # Part 1: Simulated scheduler — dependency waves and timing
42
- # =============================================================================
43
- #
44
- # Pipeline topology:
45
- #
46
- # headline_finder ─────────────────────────────────┐
47
- # ├──► report_writer
48
- # background_brief ─────────────────────────────────┤
49
- # │
50
- # fact_checker ─────────────────────────────────┘
51
- #
52
- # Wave 1 — headline_finder, background_brief, fact_checker: parallel (none depend on each other)
53
- # Wave 2 — report_writer: sequential (depends on all three)
54
- #
55
- # Simulated latencies (seconds):
56
- LATENCIES = {
57
- "headline_finder" => 0.60,
58
- "background_brief" => 0.50,
59
- "fact_checker" => 0.40,
60
- "report_writer" => 0.70
61
- }.freeze
62
- #
63
- # Expected times:
64
- # Parallel — wave1 = max(0.60, 0.50, 0.40) = 0.60s
65
- # wave2 = 0.70s
66
- # total ≈ 1.30s
67
- #
68
- # Sequential — 0.60 + 0.50 + 0.40 + 0.70 = 2.20s
69
- #
70
- # Speedup ≈ 1.7×
71
-
72
- section "Part 1: Simulated Parallel Run (no LLM traffic)"
73
-
74
- # SimulatedScheduler overrides execute_spec so that instead of
75
- # constructing a real Robot and calling the LLM, it just sleeps for
76
- # the robot's configured latency and returns a synthetic result string.
77
- # All dependency-ordering and wave-dispatch logic is inherited unchanged.
78
- class SimulatedScheduler < RobotLab::RactorNetworkScheduler
79
- private
80
-
81
- def execute_spec(spec, _message)
82
- delay = LATENCIES[spec.name] || 0.30
83
- sleep delay
84
- "#{spec.name}: completed after #{delay}s".freeze
85
- end
86
- end
87
-
88
- # Build RobotSpec objects directly.
89
- # (Normally Network#run_with_ractor_scheduler builds these from Task wrappers.)
90
- def make_spec(name, deps = :none)
91
- spec = RobotLab::RobotSpec.new(
92
- name: name.freeze,
93
- template: nil,
94
- system_prompt: "You are the #{name} robot.".freeze,
95
- config_hash: {}.freeze
96
- )
97
- { spec: spec, depends_on: deps }
98
- end
99
-
100
- specs_with_deps = [
101
- make_spec("headline_finder"),
102
- make_spec("background_brief"),
103
- make_spec("fact_checker"),
104
- make_spec("report_writer", ["headline_finder", "background_brief", "fact_checker"])
105
- ]
106
-
107
- memory = RobotLab::Memory.new
108
- scheduler = SimulatedScheduler.new(memory: memory)
109
-
110
- topic = "artificial intelligence regulation in 2025"
111
-
112
- t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
113
- results = scheduler.run_pipeline(specs_with_deps, message: topic)
114
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
115
-
116
- scheduler.shutdown
117
-
118
- sequential_total = LATENCIES.values.sum
119
-
120
- puts " Pipeline topology:"
121
- puts " Wave 1 (parallel): headline_finder · background_brief · fact_checker"
122
- puts " Wave 2 (sequential): report_writer ← waits for all three"
123
- puts
124
-
125
- puts " Results (Hash { name => result_string }):"
126
- results.each { |name, val| puts " \"#{name}\" => #{val.inspect}" }
127
- puts
128
-
129
- puts " Wall time: #{"%.3f" % elapsed}s"
130
- puts " Sequential equiv: #{"%.3f" % sequential_total}s (#{LATENCIES.map { |n, d| "#{n}=#{d}" }.join(' + ')})"
131
- puts " Speedup: #{(sequential_total / elapsed).round(1)}×"
132
- puts
133
-
134
- # =============================================================================
135
- # Part 2: Network.new(parallel_mode: :ractor) API
136
- # =============================================================================
137
-
138
- section "Part 2: Network.new(parallel_mode: :ractor)"
139
-
140
- # When parallel_mode: :ractor is set on a Network, network.run(message:)
141
- # routes through RactorNetworkScheduler instead of SimpleFlow::Pipeline.
142
- # The default mode is :async (unchanged SimpleFlow behavior).
143
-
144
- network = RobotLab::Network.new(name: "research_pipeline", parallel_mode: :ractor) do
145
- task :headline_finder, RobotLab.build(name: "headline_finder",
146
- system_prompt: "Find the 3 most relevant news headlines. Be concise.",
147
- **llm_opts),
148
- depends_on: :none
149
-
150
- task :background_brief, RobotLab.build(name: "background_brief",
151
- system_prompt: "Provide a 2-sentence background on the topic.",
152
- **llm_opts),
153
- depends_on: :none
154
-
155
- task :fact_checker, RobotLab.build(name: "fact_checker",
156
- system_prompt: "List 3 verifiable facts about the topic.",
157
- **llm_opts),
158
- depends_on: :none
159
-
160
- task :report_writer, RobotLab.build(name: "report_writer",
161
- system_prompt: "Synthesize the provided context into a 3-sentence report.",
162
- **llm_opts),
163
- depends_on: ["headline_finder", "background_brief", "fact_checker"]
164
- end
165
-
166
- puts " network.name => #{network.name.inspect}"
167
- puts " network.parallel_mode => #{network.parallel_mode.inspect}"
168
- puts
169
- puts " Dependency graph (from network.pipeline.step_dependencies):"
170
-
171
- network.pipeline.step_dependencies.each do |step, deps|
172
- dep_str = deps.empty? ? "(entry point — Wave 1)" : "depends on: #{deps.join(', ')}"
173
- puts " :#{step.to_s.ljust(18)} #{dep_str}"
174
- end
175
- puts
176
- puts " When network.run(message: ...) is called:"
177
- puts " • RactorNetworkScheduler is created with the network's memory"
178
- puts " • Each task is converted to a frozen RobotSpec"
179
- puts " • Wave 1 tasks are dispatched concurrently (Thread per task)"
180
- puts " • Each Thread submits a RactorJob to the worker queue"
181
- puts " • A Ractor worker pops the job, constructs a fresh Robot from the"
182
- puts " spec, calls robot.run(message), and returns the frozen result"
183
- puts " • Wave 2 begins once all Wave 1 results are available"
184
- puts " • Return value is a Hash { robot_name => result_string }"
185
- puts
186
-
187
- # =============================================================================
188
- # Part 3: Live LLM run (opt-in via RUN_LIVE=1)
189
- # =============================================================================
190
-
191
- unless ENV["RUN_LIVE"]
192
- section "Part 3: Live LLM Run"
193
- puts " Set RUN_LIVE=1 (with LM Studio running) to attempt the real pipeline."
194
- puts " Expected behavior: headline_finder, background_brief, and"
195
- puts " fact_checker run in parallel; report_writer follows."
196
- puts
197
- puts " Heads-up: this is expected to fail today — see the rescue clauses"
198
- puts " below. ruby_llm keeps Procs in class-level state, which cannot"
199
- puts " cross a Ractor boundary. Part 1 shows the wave ordering that Part 3"
200
- puts " will exercise once ruby_llm is Ractor-safe."
201
- puts
202
- hr
203
- puts "Example 30 complete."
204
- exit 0
205
- end
206
-
207
- require_lms!
208
-
209
- section "Part 3: Live LLM Run (RUN_LIVE set)"
210
-
211
- puts " Running 4-robot research pipeline on:"
212
- puts " \"#{topic}\""
213
- puts
214
- puts " Wave 1 (parallel): headline_finder · background_brief · fact_checker"
215
- puts " Wave 2: report_writer"
216
- puts
217
-
218
- t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
219
-
220
- begin
221
- result = network.run(message: topic)
222
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
223
-
224
- puts " Completed in #{"%.2f" % elapsed}s"
225
- puts
226
- puts " Results:"
227
- result.each do |name, text|
228
- snippet = text.to_s.strip.lines.first.to_s.strip
229
- puts " [#{name}] #{snippet[0..90]}#{snippet.length > 90 ? '…' : ''}"
230
- end
231
-
232
- rescue Ractor::IsolationError, Ractor::Error => e
233
- puts " Ractor error: #{e.class}: #{e.message[0..100]}"
234
- puts
235
- puts " ruby_llm is not yet Ractor-safe: it uses Procs in class"
236
- puts " definitions that cannot cross Ractor boundaries."
237
- puts " Track this at: https://github.com/crmne/ruby_llm"
238
-
239
- rescue RobotLab::Error => e
240
- if e.message.include?("un-shareable Proc")
241
- puts " Live LLM runs require ruby_llm to be Ractor-safe."
242
- puts " ruby_llm stores Procs in class-level hooks that cannot cross"
243
- puts " Ractor boundaries. Part 1 (SimulatedScheduler) demonstrates wave"
244
- puts " ordering today; Part 3 will work once ruby_llm gains Ractor support."
245
- else
246
- puts " RobotLab::Error: #{e.message}"
247
- end
248
- end
249
-
250
- puts
251
- hr
252
- puts "Example 30 complete."
@@ -1,246 +0,0 @@
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
- # 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
- **llm_opts,
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 = collect_findings
115
-
116
- prompt = <<~PROMPT
117
- Six specialist analysts have reviewed our product launch readiness.
118
- Based on their findings, issue a final GO or NO-GO recommendation
119
- in 3-5 sentences. Be direct and specific about the key deciding factors.
120
-
121
- Market analysis: #{findings[:market] || "(not received)"}
122
- Competitive analysis: #{findings[:competitive] || "(not received)"}
123
- Technical review: #{findings[:tech] || "(not received)"}
124
- Risk assessment: #{findings[:risk] || "(not received)"}
125
- Financial review: #{findings[:financial] || "(not received)"}
126
- Legal review: #{findings[:legal] || "(not received)"}
127
-
128
- Begin your response with "GO -" or "NO-GO -".
129
- PROMPT
130
-
131
- recommendation = run(prompt).reply.strip
132
- @shared_memory.set(:recommendation, recommendation)
133
- puts " [#{name}] recommendation ready"
134
-
135
- result.with_context(:recommendation, recommendation).continue(recommendation)
136
- end
137
-
138
- private
139
-
140
- # Memory#get(wait:) RAISES RobotLab::AwaitTimeout on expiry — it never
141
- # returns a :timeout sentinel. Rescue it and re-read without wait:, which
142
- # never blocks and omits keys that were never written, so a stalled analyst
143
- # degrades the recommendation instead of killing the director.
144
- def collect_findings
145
- @shared_memory.get(*FINDING_KEYS, wait: 120)
146
- rescue RobotLab::AwaitTimeout => e
147
- puts " [#{name}] WARNING: #{e.message}"
148
- partial = @shared_memory.get(*FINDING_KEYS)
149
- missing = FINDING_KEYS - partial.keys
150
- puts " [#{name}] Proceeding without: #{missing.join(', ')}" if missing.any?
151
- partial
152
- end
153
- end
154
-
155
- # ── Product Brief ───────────────────────────────────────────────────────────────
156
-
157
- PRODUCT_BRIEF = <<~BRIEF
158
- Product: "Orion" — an AI-powered project management tool that auto-generates
159
- sprint plans from Jira backlogs, detects scope creep in real-time, and integrates
160
- with GitHub and Slack via webhooks. SaaS pricing: $25/seat/month, 14-day free trial.
161
- Target: mid-size engineering teams (20-200 developers). Launch date: 6 weeks out.
162
- Beta: 12 paying customers, 94% satisfaction, 0 critical bugs open. SOC 2 Type I
163
- certification in progress, expected within 30 days.
164
- BRIEF
165
-
166
- # ── Build the six analysts ───────────────────────────────────────────────────────
167
-
168
- ANALYSTS = [
169
- { name: "market_analyst", key: :market, role: "market opportunity analyst" },
170
- { name: "competitive_analyst", key: :competitive, role: "competitive intelligence analyst" },
171
- { name: "tech_reviewer", key: :tech, role: "technical readiness and quality reviewer" },
172
- { name: "risk_assessor", key: :risk, role: "product risk assessment specialist" },
173
- { name: "financial_reviewer", key: :financial, role: "financial viability and pricing analyst" },
174
- { name: "legal_reviewer", key: :legal, role: "legal, compliance, and IP reviewer" },
175
- ].freeze
176
-
177
- analyst_robots = ANALYSTS.map do |spec|
178
- AnalystRobot.new(name: spec[:name], memory_key: spec[:key], role: spec[:role])
179
- end
180
-
181
- director = LaunchDirector.new(
182
- name: "launch_director",
183
- **llm_opts,
184
- system_prompt: "You are the VP of Product making the final launch call."
185
- )
186
-
187
- # ── Network — note the concurrency cap ──────────────────────────────────────────
188
-
189
- config = RobotLab::RunConfig.new(max_concurrent_robots: 4)
190
-
191
- analyst_names = analyst_robots.map { |r| r.name.to_sym }
192
-
193
- network = RobotLab.create_network(name: "launch_assessment", config: config) do
194
- analyst_robots.each do |robot|
195
- task robot.name.to_sym, robot, depends_on: :none
196
- end
197
-
198
- task :launch_director, director, depends_on: analyst_names
199
- end
200
-
201
- # Assign shared memory so each robot can write to it directly
202
- shared_memory = network.memory
203
- (analyst_robots + [director]).each { |r| r.shared_memory = shared_memory }
204
-
205
- # Subscribe for a memory-level audit trail
206
- analyst_robots.each do |robot|
207
- network.memory.subscribe(robot.memory_key) do |change|
208
- puts " [memory] :#{change.key} written by #{change.writer}"
209
- end
210
- end
211
-
212
- # ── Run ─────────────────────────────────────────────────────────────────────────
213
-
214
- banner "Product Launch Assessment"
215
- puts " 6 specialist analysts in parallel, max_concurrent_robots: 4"
216
- puts
217
- puts "Pipeline:"
218
- puts network.visualize
219
- puts
220
- puts "Concurrency config: #{config.inspect}"
221
- puts
222
- puts "Product brief:"
223
- puts PRODUCT_BRIEF.strip.gsub(/^/, " ")
224
- puts
225
- section "Running — Analysts 5 and 6 Queue Until a Semaphore Slot Opens"
226
-
227
- $run_start = Time.now
228
- result = network.run(message: PRODUCT_BRIEF)
229
- elapsed = "%.1f" % (Time.now - $run_start)
230
-
231
- puts
232
- hr
233
- puts "All analysts complete. Total wall time: #{elapsed}s"
234
- hr
235
-
236
- section "Launch Director Recommendation"
237
- puts network.memory[:recommendation]
238
- puts
239
- section "Individual Analyst Verdicts"
240
- analyst_robots.each do |robot|
241
- label = robot.name.gsub("_", " ").upcase
242
- finding = network.memory.get(robot.memory_key).to_s
243
- puts "#{label}"
244
- puts finding
245
- puts
246
- end
@@ -1,193 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Example 32: Newsletter Issue Retriever
5
- #
6
- # NOTE: this is a plain utility script, not a RobotLab demo — it uses no part
7
- # of the library. It lives here as the content feeder for other experiments.
8
- # Read examples 01-31 for library capabilities.
9
- #
10
- # Fetches all unprocessed issues from multiple Ruby newsletters via RSS
11
- # and saves each as a Markdown file in a Markdown clippings folder.
12
- #
13
- # - Processes oldest unprocessed issue first (across all feeds)
14
- # - Filename: <newsletter-name>_YYYYMMDD.md
15
- # - Includes YAML frontmatter with source URL (compatible with Clippings workflow)
16
- # - Tracks processed issue URLs in ~/.robot_lab/newsletter_processed.yaml
17
- #
18
- # Usage:
19
- # ruby examples/32_newsletter_reader.rb
20
- # CLIPPINGS_DIR=~/Notes/Clippings ruby examples/32_newsletter_reader.rb
21
-
22
- require "net/http"
23
- require "open3"
24
- require "time"
25
- require "uri"
26
- require "yaml"
27
- require "fileutils"
28
- require "rexml/document"
29
- require "rexml/xpath"
30
-
31
- NEWSLETTER_RSS_URLS = [
32
- "https://rss.beehiiv.com/feeds/MTJunJRFxo.xml", # Ruby AI News
33
- "https://cprss.s3.amazonaws.com/rubyweekly.com.xml" # Ruby Weekly
34
- ].freeze
35
-
36
- CLIPPINGS_DIR = File.expand_path(ENV.fetch("CLIPPINGS_DIR", "~/Documents/Clippings"))
37
- PROCESSED_STATE_FILE = File.join(Dir.home, ".robot_lab", "newsletter_processed.yaml")
38
-
39
- # Tracks which newsletter issue URLs have been saved.
40
- class ProcessedIssues
41
- def initialize(path: PROCESSED_STATE_FILE)
42
- @path = path
43
- @urls = load_urls
44
- end
45
-
46
- def processed?(url)
47
- @urls.include?(url)
48
- end
49
-
50
- def mark_processed(url)
51
- @urls << url
52
- FileUtils.mkdir_p(File.dirname(@path))
53
- File.write(@path, YAML.dump(@urls.uniq))
54
- end
55
-
56
- def count = @urls.size
57
-
58
- private
59
-
60
- def load_urls
61
- return [] unless File.exist?(@path)
62
- Array(YAML.safe_load(File.read(@path)) || [])
63
- end
64
- end
65
-
66
- # Fetches a single RSS feed URL and returns its items (unsorted).
67
- # Each item: { title:, url:, pub_date:, published_at:, html:, channel_name: }
68
- def fetch_rss_items(url)
69
- uri = URI(url)
70
- response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |h| h.get(uri.request_uri) }
71
- raise "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
72
-
73
- doc = REXML::Document.new(response.body)
74
- ns = { "content" => "http://purl.org/rss/1.0/modules/content/" }
75
-
76
- channel_title = REXML::XPath.first(doc, "//channel/title")&.text&.strip || "newsletter"
77
-
78
- REXML::XPath.match(doc, "//channel/item").filter_map do |item|
79
- item_url = REXML::XPath.first(item, "link")&.text&.strip
80
- title = REXML::XPath.first(item, "title")&.text&.strip
81
- pub_date = REXML::XPath.first(item, "pubDate")&.text&.strip
82
- html = REXML::XPath.first(item, "content:encoded", ns)&.text ||
83
- REXML::XPath.first(item, "description")&.text || ""
84
-
85
- next unless item_url && title && pub_date
86
-
87
- {
88
- title: title,
89
- url: item_url,
90
- pub_date: pub_date,
91
- published_at: Time.parse(pub_date),
92
- html: html,
93
- channel_name: channel_title
94
- }
95
- end
96
- end
97
-
98
- # Fetches all configured RSS feeds and returns items sorted oldest-first.
99
- def fetch_all_rss_items
100
- NEWSLETTER_RSS_URLS.flat_map do |feed_url|
101
- print " #{feed_url}... "
102
- items = fetch_rss_items(feed_url)
103
- puts "#{items.size} issues"
104
- items
105
- end.sort_by { |item| item[:published_at] }
106
- end
107
-
108
- # Converts HTML to Markdown via html2markdown CLI, stripping UTM params.
109
- def html_to_markdown(html)
110
- md, = Open3.capture3(
111
- "html2markdown",
112
- stdin_data: html
113
- )
114
- md.gsub(/\]\(([^)]+)\)/) do
115
- url = $1
116
- if url.include?("?")
117
- base, query = url.split("?", 2)
118
- kept = query.split("&").reject { |p| p.start_with?("utm_") }
119
- "](#{kept.empty? ? base : "#{base}?#{kept.join("&")}"})"
120
- else
121
- "](#{url})"
122
- end
123
- end
124
- end
125
-
126
- # Builds the output filename: <newsletter-name>_YYYYMMDD.md
127
- def output_filename(channel_name, published_at)
128
- safe_name = channel_name.downcase.gsub(/[^a-z0-9]+/, "_").delete_suffix("_")
129
- date_str = published_at.strftime("%Y%m%d")
130
- "#{safe_name}_#{date_str}.md"
131
- end
132
-
133
- # Wraps markdown content in YAML frontmatter compatible with the Clippings workflow.
134
- def wrap_with_frontmatter(title:, url:, pub_date:, body:)
135
- date = Time.parse(pub_date).strftime("%Y-%m-%d")
136
- <<~MD
137
- ---
138
- source: #{url}
139
- title: "#{title.gsub('"', '\\"')}"
140
- date: #{date}
141
- ---
142
-
143
- # #{title}
144
-
145
- #{body.strip}
146
- MD
147
- end
148
-
149
- # ── Main ──────────────────────────────────────────────────────────────────────
150
-
151
- puts "=" * 60
152
- puts "Example 32: Newsletter Issue Retriever"
153
- puts "=" * 60
154
- puts
155
-
156
- FileUtils.mkdir_p(CLIPPINGS_DIR)
157
- state = ProcessedIssues.new
158
-
159
- puts "Fetching #{NEWSLETTER_RSS_URLS.size} RSS feeds..."
160
- all_items = fetch_all_rss_items
161
- pending = all_items.reject { |item| state.processed?(item[:url]) }
162
- puts "#{all_items.size} total issues found across all feeds."
163
-
164
- if pending.empty?
165
- puts "All issues already saved. Nothing to do."
166
- exit
167
- end
168
-
169
- puts "#{pending.size} unprocessed (#{state.count} already done). Saving oldest-first."
170
- puts
171
-
172
- pending.each_with_index do |item, idx|
173
- filename = output_filename(item[:channel_name], item[:published_at])
174
- filepath = File.join(CLIPPINGS_DIR, filename)
175
-
176
- print "[#{idx + 1}/#{pending.size}] #{item[:title]} (#{item[:published_at].strftime("%Y-%m-%d")})... "
177
-
178
- body = html_to_markdown(item[:html])
179
- content = wrap_with_frontmatter(
180
- title: item[:title],
181
- url: item[:url],
182
- pub_date: item[:pub_date],
183
- body: body
184
- )
185
-
186
- File.write(filepath, content)
187
- state.mark_processed(item[:url])
188
-
189
- puts "saved → #{filename}"
190
- end
191
-
192
- puts
193
- puts "Done. #{pending.size} issue(s) saved to #{CLIPPINGS_DIR}"