robot_lab 0.3.0 → 0.5.0

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 (274) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/README.md +29 -19
  4. data/lib/robot_lab/errors.rb +4 -0
  5. data/lib/robot_lab/hook_context.rb +9 -2
  6. data/lib/robot_lab/mcp/registry.rb +99 -0
  7. data/lib/robot_lab/mcp/server.rb +47 -83
  8. data/lib/robot_lab/mcp/server_discovery.rb +22 -9
  9. data/lib/robot_lab/mcp.rb +259 -0
  10. data/lib/robot_lab/robot/mcp_management.rb +159 -105
  11. data/lib/robot_lab/robot/tool_call_gating.rb +144 -0
  12. data/lib/robot_lab/robot.rb +72 -99
  13. data/lib/robot_lab/run_config.rb +3 -2
  14. data/lib/robot_lab/tool.rb +8 -23
  15. data/lib/robot_lab/version.rb +1 -1
  16. data/lib/robot_lab.rb +1 -4
  17. metadata +16 -292
  18. data/.architecture/AGENTS.md +0 -32
  19. data/.architecture/config.yml +0 -8
  20. data/.architecture/members.yml +0 -60
  21. data/.architecture/reviews/feature-free-will.md +0 -490
  22. data/.architecture/reviews/overall-codebase.md +0 -427
  23. data/.claude/settings.local.json +0 -57
  24. data/.codex/config.toml +0 -2
  25. data/.envrc +0 -6
  26. data/.github/workflows/deploy-github-pages.yml +0 -52
  27. data/.irbrc +0 -6
  28. data/.loki +0 -65
  29. data/.quality/reek_baseline.txt +0 -43
  30. data/.rubocop.yml +0 -10
  31. data/Archspec.rb +0 -44
  32. data/CLAUDE.md +0 -141
  33. data/COMMITS.md +0 -196
  34. data/Rakefile +0 -193
  35. data/_typos.toml +0 -21
  36. data/agent2agent_review.md +0 -192
  37. data/agentf_improvements.md +0 -253
  38. data/agents.md +0 -14
  39. data/docs/api/core/index.md +0 -147
  40. data/docs/api/core/memory.md +0 -578
  41. data/docs/api/core/network.md +0 -553
  42. data/docs/api/core/result.md +0 -211
  43. data/docs/api/core/robot.md +0 -1667
  44. data/docs/api/core/state.md +0 -145
  45. data/docs/api/core/tool.md +0 -520
  46. data/docs/api/errors.md +0 -323
  47. data/docs/api/hooks.md +0 -469
  48. data/docs/api/index.md +0 -137
  49. data/docs/api/mcp/client.md +0 -387
  50. data/docs/api/mcp/index.md +0 -267
  51. data/docs/api/mcp/server.md +0 -252
  52. data/docs/api/mcp/transports.md +0 -339
  53. data/docs/api/messages/index.md +0 -126
  54. data/docs/api/messages/text-message.md +0 -170
  55. data/docs/api/messages/tool-call-message.md +0 -206
  56. data/docs/api/messages/tool-result-message.md +0 -242
  57. data/docs/api/messages/user-message.md +0 -232
  58. data/docs/api/skills.md +0 -423
  59. data/docs/api/streaming/context.md +0 -280
  60. data/docs/api/streaming/events.md +0 -185
  61. data/docs/api/streaming/index.md +0 -156
  62. data/docs/api/support.md +0 -846
  63. data/docs/architecture/core-concepts.md +0 -575
  64. data/docs/architecture/index.md +0 -252
  65. data/docs/architecture/message-flow.md +0 -366
  66. data/docs/architecture/network-orchestration.md +0 -478
  67. data/docs/architecture/robot-execution.md +0 -396
  68. data/docs/architecture/state-management.md +0 -410
  69. data/docs/assets/css/custom.css +0 -56
  70. data/docs/assets/images/robot_lab.jpg +0 -0
  71. data/docs/concepts.md +0 -641
  72. data/docs/examples/basic-chat.md +0 -258
  73. data/docs/examples/index.md +0 -425
  74. data/docs/examples/mcp-server.md +0 -403
  75. data/docs/examples/multi-robot-network.md +0 -476
  76. data/docs/examples/tool-usage.md +0 -468
  77. data/docs/getting-started/configuration.md +0 -645
  78. data/docs/getting-started/index.md +0 -56
  79. data/docs/getting-started/installation.md +0 -197
  80. data/docs/getting-started/quick-start.md +0 -293
  81. data/docs/guides/building-robots.md +0 -1187
  82. data/docs/guides/creating-networks.md +0 -736
  83. data/docs/guides/hooks.md +0 -1206
  84. data/docs/guides/index.md +0 -78
  85. data/docs/guides/knowledge.md +0 -219
  86. data/docs/guides/mcp-integration.md +0 -740
  87. data/docs/guides/memory.md +0 -458
  88. data/docs/guides/observability.md +0 -733
  89. data/docs/guides/streaming.md +0 -323
  90. data/docs/guides/using-tools.md +0 -780
  91. data/docs/index.md +0 -280
  92. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  93. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  94. data/examples/.envrc +0 -3
  95. data/examples/01_simple_robot.rb +0 -31
  96. data/examples/02_tools.rb +0 -109
  97. data/examples/03_network.rb +0 -118
  98. data/examples/04_mcp.rb +0 -217
  99. data/examples/05_streaming.rb +0 -137
  100. data/examples/06_prompt_templates.rb +0 -325
  101. data/examples/07_network_memory.rb +0 -313
  102. data/examples/08_llm_config.rb +0 -162
  103. data/examples/09_chaining.rb +0 -251
  104. data/examples/10_memory.rb +0 -315
  105. data/examples/11_network_introspection.rb +0 -239
  106. data/examples/12_message_bus.rb +0 -67
  107. data/examples/13_spawn.rb +0 -85
  108. data/examples/14_rusty_circuit/.envrc +0 -1
  109. data/examples/14_rusty_circuit/.gitignore +0 -1
  110. data/examples/14_rusty_circuit/README.md +0 -1
  111. data/examples/14_rusty_circuit/comic.rb +0 -149
  112. data/examples/14_rusty_circuit/display.rb +0 -217
  113. data/examples/14_rusty_circuit/heckler.rb +0 -65
  114. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  115. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  116. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  117. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  118. data/examples/14_rusty_circuit/scout.rb +0 -165
  119. data/examples/14_rusty_circuit/show.log +0 -234
  120. data/examples/15_memory_network_and_bus/.envrc +0 -1
  121. data/examples/15_memory_network_and_bus/README.md +0 -66
  122. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  123. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  124. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  125. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  126. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  127. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
  128. data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
  129. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  130. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  131. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  132. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
  133. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
  134. data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
  135. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  136. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  137. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  138. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  139. data/examples/16_writers_room/.envrc +0 -1
  140. data/examples/16_writers_room/display.rb +0 -158
  141. data/examples/16_writers_room/output/.gitignore +0 -4
  142. data/examples/16_writers_room/output/README.md +0 -69
  143. data/examples/16_writers_room/output/opus_001.md +0 -263
  144. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  145. data/examples/16_writers_room/output/opus_002.md +0 -245
  146. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  147. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  148. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  149. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  150. data/examples/16_writers_room/prompts/writer.md +0 -37
  151. data/examples/16_writers_room/room.rb +0 -186
  152. data/examples/16_writers_room/tools.rb +0 -173
  153. data/examples/16_writers_room/writer.rb +0 -121
  154. data/examples/16_writers_room/writers_room.rb +0 -256
  155. data/examples/17_skills.rb +0 -234
  156. data/examples/18_rails/.envrc +0 -3
  157. data/examples/18_rails/.gitignore +0 -5
  158. data/examples/18_rails/Gemfile +0 -11
  159. data/examples/18_rails/README.md +0 -67
  160. data/examples/18_rails/Rakefile +0 -4
  161. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  162. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  163. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  164. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  165. data/examples/18_rails/app/models/application_record.rb +0 -5
  166. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  167. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  168. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  169. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  170. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  171. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  172. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  173. data/examples/18_rails/bin/dev +0 -7
  174. data/examples/18_rails/bin/rails +0 -6
  175. data/examples/18_rails/bin/setup +0 -15
  176. data/examples/18_rails/config/application.rb +0 -33
  177. data/examples/18_rails/config/cable.yml +0 -2
  178. data/examples/18_rails/config/database.yml +0 -5
  179. data/examples/18_rails/config/environment.rb +0 -4
  180. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  181. data/examples/18_rails/config/routes.rb +0 -6
  182. data/examples/18_rails/config.ru +0 -4
  183. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  184. data/examples/19_token_tracking.rb +0 -138
  185. data/examples/20_circuit_breaker.rb +0 -147
  186. data/examples/21_learning_loop.rb +0 -181
  187. data/examples/22_context_compression.rb +0 -172
  188. data/examples/23_convergence.rb +0 -133
  189. data/examples/24_structured_delegation.rb +0 -151
  190. data/examples/25_history_search/conversation.jsonl +0 -30
  191. data/examples/25_history_search.rb +0 -133
  192. data/examples/26_document_store/api_versioning_adr.md +0 -52
  193. data/examples/26_document_store/incident_postmortem.md +0 -46
  194. data/examples/26_document_store/postgres_runbook.md +0 -49
  195. data/examples/26_document_store/redis_caching_guide.md +0 -48
  196. data/examples/26_document_store/sidekiq_guide.md +0 -51
  197. data/examples/26_document_store.rb +0 -140
  198. data/examples/27_incident_response/README.md +0 -65
  199. data/examples/27_incident_response/incident_response.rb +0 -261
  200. data/examples/28_mcp_discovery.rb +0 -113
  201. data/examples/29_ractor_tools.rb +0 -240
  202. data/examples/30_ractor_network.rb +0 -252
  203. data/examples/31_launch_assessment.rb +0 -246
  204. data/examples/32_newsletter_reader.rb +0 -193
  205. data/examples/33_stock_generator.rb +0 -80
  206. data/examples/33_stock_predictor.rb +0 -327
  207. data/examples/34_agentskills.rb +0 -76
  208. data/examples/35_hooks.rb +0 -266
  209. data/examples/README.md +0 -550
  210. data/examples/common.rb +0 -180
  211. data/examples/prompts/assistant.md +0 -5
  212. data/examples/prompts/audit_trail.md +0 -8
  213. data/examples/prompts/billing.md +0 -10
  214. data/examples/prompts/classifier.md +0 -7
  215. data/examples/prompts/comedian.md +0 -6
  216. data/examples/prompts/comedy_critic.md +0 -10
  217. data/examples/prompts/configurable.md +0 -9
  218. data/examples/prompts/dispatcher.md +0 -12
  219. data/examples/prompts/entity_extractor.md +0 -14
  220. data/examples/prompts/escalation.md +0 -42
  221. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  222. data/examples/prompts/frontmatter_named_test.md +0 -5
  223. data/examples/prompts/frontmatter_tools_test.md +0 -6
  224. data/examples/prompts/general.md +0 -7
  225. data/examples/prompts/github_assistant.md +0 -14
  226. data/examples/prompts/helper.md +0 -4
  227. data/examples/prompts/incident_responder.md +0 -18
  228. data/examples/prompts/keyword_extractor.md +0 -11
  229. data/examples/prompts/llm_config_demo.md +0 -20
  230. data/examples/prompts/order_support.md +0 -35
  231. data/examples/prompts/os_advocate.md +0 -13
  232. data/examples/prompts/os_chief.md +0 -13
  233. data/examples/prompts/os_editor.md +0 -13
  234. data/examples/prompts/parameterized_main_test.md +0 -6
  235. data/examples/prompts/pii_redactor.md +0 -7
  236. data/examples/prompts/product_support.md +0 -37
  237. data/examples/prompts/runbook_protocol.md +0 -12
  238. data/examples/prompts/sentiment_analyzer.md +0 -12
  239. data/examples/prompts/skill_a_test.md +0 -4
  240. data/examples/prompts/skill_b_test.md +0 -4
  241. data/examples/prompts/skill_config_test.md +0 -5
  242. data/examples/prompts/skill_cycle_a_test.md +0 -6
  243. data/examples/prompts/skill_cycle_b_test.md +0 -6
  244. data/examples/prompts/skill_description_test.md +0 -4
  245. data/examples/prompts/skill_leaf_test.md +0 -4
  246. data/examples/prompts/skill_nested_test.md +0 -6
  247. data/examples/prompts/skill_refs_main_test.md +0 -6
  248. data/examples/prompts/skill_self_ref_test.md +0 -6
  249. data/examples/prompts/skill_with_mcp_test.md +0 -9
  250. data/examples/prompts/skill_with_params_test.md +0 -6
  251. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  252. data/examples/prompts/skill_with_tools_test.md +0 -6
  253. data/examples/prompts/sre_compliance.md +0 -10
  254. data/examples/prompts/structured_output.md +0 -7
  255. data/examples/prompts/synthesizer.md +0 -17
  256. data/examples/prompts/technical.md +0 -10
  257. data/examples/prompts/template_with_skills_test.md +0 -6
  258. data/examples/prompts/triage.md +0 -22
  259. data/examples/ruboruby.md +0 -423
  260. data/examples/run_all.rb +0 -60
  261. data/examples/xyzzy.rb +0 -97
  262. data/lib/robot_lab/mcp/client.rb +0 -223
  263. data/lib/robot_lab/mcp/connection_poller.rb +0 -195
  264. data/lib/robot_lab/mcp/transports/base.rb +0 -64
  265. data/lib/robot_lab/mcp/transports/sse.rb +0 -120
  266. data/lib/robot_lab/mcp/transports/stdio.rb +0 -170
  267. data/lib/robot_lab/mcp/transports/streamable_http.rb +0 -141
  268. data/lib/robot_lab/mcp/transports/websocket.rb +0 -111
  269. data/logfile +0 -8
  270. data/mkdocs.yml +0 -209
  271. data/robot_concurrency.md +0 -38
  272. data/simple_acp_review.md +0 -298
  273. data/temp.md +0 -6
  274. 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}"