robot_lab 0.2.8 → 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 (262) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/README.md +16 -18
  4. data/lib/robot_lab/ask_user.rb +3 -3
  5. data/lib/robot_lab/config/defaults.yml +5 -5
  6. data/lib/robot_lab/config.rb +2 -2
  7. data/lib/robot_lab/mcp/transports/streamable_http.rb +2 -4
  8. data/lib/robot_lab/memory.rb +16 -7
  9. data/lib/robot_lab/robot/hooking.rb +26 -0
  10. data/lib/robot_lab/robot/result_building.rb +119 -0
  11. data/lib/robot_lab/robot.rb +37 -120
  12. data/lib/robot_lab/run_config.rb +52 -20
  13. data/lib/robot_lab/tool.rb +7 -12
  14. data/lib/robot_lab/version.rb +1 -1
  15. data/lib/robot_lab.rb +3 -3
  16. metadata +15 -274
  17. data/.architecture/AGENTS.md +0 -32
  18. data/.architecture/config.yml +0 -8
  19. data/.architecture/members.yml +0 -60
  20. data/.architecture/reviews/feature-free-will.md +0 -490
  21. data/.architecture/reviews/overall-codebase.md +0 -427
  22. data/.claude/settings.local.json +0 -57
  23. data/.codex/config.toml +0 -2
  24. data/.envrc +0 -6
  25. data/.github/workflows/deploy-github-pages.yml +0 -52
  26. data/.irbrc +0 -6
  27. data/.loki +0 -65
  28. data/.quality/reek_baseline.txt +0 -43
  29. data/.rubocop.yml +0 -10
  30. data/Archspec.rb +0 -44
  31. data/CLAUDE.md +0 -139
  32. data/COMMITS.md +0 -196
  33. data/Rakefile +0 -193
  34. data/_typos.toml +0 -21
  35. data/agent2agent_review.md +0 -192
  36. data/agentf_improvements.md +0 -253
  37. data/agents.md +0 -14
  38. data/docs/api/core/index.md +0 -147
  39. data/docs/api/core/memory.md +0 -578
  40. data/docs/api/core/network.md +0 -553
  41. data/docs/api/core/result.md +0 -211
  42. data/docs/api/core/robot.md +0 -1667
  43. data/docs/api/core/state.md +0 -145
  44. data/docs/api/core/tool.md +0 -520
  45. data/docs/api/errors.md +0 -323
  46. data/docs/api/hooks.md +0 -469
  47. data/docs/api/index.md +0 -137
  48. data/docs/api/mcp/client.md +0 -387
  49. data/docs/api/mcp/index.md +0 -267
  50. data/docs/api/mcp/server.md +0 -252
  51. data/docs/api/mcp/transports.md +0 -339
  52. data/docs/api/messages/index.md +0 -126
  53. data/docs/api/messages/text-message.md +0 -170
  54. data/docs/api/messages/tool-call-message.md +0 -206
  55. data/docs/api/messages/tool-result-message.md +0 -242
  56. data/docs/api/messages/user-message.md +0 -232
  57. data/docs/api/skills.md +0 -423
  58. data/docs/api/streaming/context.md +0 -280
  59. data/docs/api/streaming/events.md +0 -185
  60. data/docs/api/streaming/index.md +0 -156
  61. data/docs/api/support.md +0 -846
  62. data/docs/architecture/core-concepts.md +0 -575
  63. data/docs/architecture/index.md +0 -252
  64. data/docs/architecture/message-flow.md +0 -366
  65. data/docs/architecture/network-orchestration.md +0 -478
  66. data/docs/architecture/robot-execution.md +0 -396
  67. data/docs/architecture/state-management.md +0 -410
  68. data/docs/assets/css/custom.css +0 -56
  69. data/docs/assets/images/robot_lab.jpg +0 -0
  70. data/docs/concepts.md +0 -641
  71. data/docs/examples/basic-chat.md +0 -258
  72. data/docs/examples/index.md +0 -425
  73. data/docs/examples/mcp-server.md +0 -403
  74. data/docs/examples/multi-robot-network.md +0 -476
  75. data/docs/examples/tool-usage.md +0 -468
  76. data/docs/getting-started/configuration.md +0 -645
  77. data/docs/getting-started/index.md +0 -56
  78. data/docs/getting-started/installation.md +0 -200
  79. data/docs/getting-started/quick-start.md +0 -293
  80. data/docs/guides/building-robots.md +0 -1187
  81. data/docs/guides/creating-networks.md +0 -736
  82. data/docs/guides/hooks.md +0 -1206
  83. data/docs/guides/index.md +0 -78
  84. data/docs/guides/knowledge.md +0 -219
  85. data/docs/guides/mcp-integration.md +0 -740
  86. data/docs/guides/memory.md +0 -458
  87. data/docs/guides/observability.md +0 -733
  88. data/docs/guides/streaming.md +0 -323
  89. data/docs/guides/using-tools.md +0 -780
  90. data/docs/index.md +0 -280
  91. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  92. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  93. data/examples/.envrc +0 -1
  94. data/examples/01_simple_robot.rb +0 -31
  95. data/examples/02_tools.rb +0 -109
  96. data/examples/03_network.rb +0 -118
  97. data/examples/04_mcp.rb +0 -215
  98. data/examples/05_streaming.rb +0 -137
  99. data/examples/06_prompt_templates.rb +0 -325
  100. data/examples/07_network_memory.rb +0 -313
  101. data/examples/08_llm_config.rb +0 -162
  102. data/examples/09_chaining.rb +0 -251
  103. data/examples/10_memory.rb +0 -315
  104. data/examples/11_network_introspection.rb +0 -239
  105. data/examples/12_message_bus.rb +0 -67
  106. data/examples/13_spawn.rb +0 -85
  107. data/examples/14_rusty_circuit/.envrc +0 -1
  108. data/examples/14_rusty_circuit/.gitignore +0 -1
  109. data/examples/14_rusty_circuit/README.md +0 -1
  110. data/examples/14_rusty_circuit/comic.rb +0 -149
  111. data/examples/14_rusty_circuit/display.rb +0 -217
  112. data/examples/14_rusty_circuit/heckler.rb +0 -65
  113. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  114. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  115. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  116. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  117. data/examples/14_rusty_circuit/scout.rb +0 -165
  118. data/examples/14_rusty_circuit/show.log +0 -234
  119. data/examples/15_memory_network_and_bus/.envrc +0 -1
  120. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  121. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  122. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  123. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  124. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  125. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -13
  126. data/examples/15_memory_network_and_bus/output/final_article.md +0 -13
  127. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  128. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  129. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  130. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -18
  131. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -13
  132. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  133. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  134. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  135. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  136. data/examples/16_writers_room/.envrc +0 -1
  137. data/examples/16_writers_room/display.rb +0 -158
  138. data/examples/16_writers_room/output/.gitignore +0 -4
  139. data/examples/16_writers_room/output/README.md +0 -69
  140. data/examples/16_writers_room/output/opus_001.md +0 -263
  141. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  142. data/examples/16_writers_room/output/opus_002.md +0 -245
  143. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  144. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  145. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  146. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  147. data/examples/16_writers_room/prompts/writer.md +0 -37
  148. data/examples/16_writers_room/room.rb +0 -186
  149. data/examples/16_writers_room/tools.rb +0 -173
  150. data/examples/16_writers_room/writer.rb +0 -121
  151. data/examples/16_writers_room/writers_room.rb +0 -256
  152. data/examples/17_skills.rb +0 -234
  153. data/examples/18_rails/.envrc +0 -3
  154. data/examples/18_rails/.gitignore +0 -5
  155. data/examples/18_rails/Gemfile +0 -11
  156. data/examples/18_rails/README.md +0 -67
  157. data/examples/18_rails/Rakefile +0 -4
  158. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  159. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  160. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  161. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  162. data/examples/18_rails/app/models/application_record.rb +0 -5
  163. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  164. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  165. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  166. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  167. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  168. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  169. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  170. data/examples/18_rails/bin/dev +0 -7
  171. data/examples/18_rails/bin/rails +0 -6
  172. data/examples/18_rails/bin/setup +0 -15
  173. data/examples/18_rails/config/application.rb +0 -33
  174. data/examples/18_rails/config/cable.yml +0 -2
  175. data/examples/18_rails/config/database.yml +0 -5
  176. data/examples/18_rails/config/environment.rb +0 -4
  177. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  178. data/examples/18_rails/config/routes.rb +0 -6
  179. data/examples/18_rails/config.ru +0 -4
  180. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  181. data/examples/19_token_tracking.rb +0 -138
  182. data/examples/20_circuit_breaker.rb +0 -147
  183. data/examples/21_learning_loop.rb +0 -181
  184. data/examples/22_context_compression.rb +0 -172
  185. data/examples/23_convergence.rb +0 -133
  186. data/examples/24_structured_delegation.rb +0 -151
  187. data/examples/25_history_search/conversation.jsonl +0 -30
  188. data/examples/25_history_search.rb +0 -133
  189. data/examples/26_document_store/api_versioning_adr.md +0 -52
  190. data/examples/26_document_store/incident_postmortem.md +0 -46
  191. data/examples/26_document_store/postgres_runbook.md +0 -49
  192. data/examples/26_document_store/redis_caching_guide.md +0 -48
  193. data/examples/26_document_store/sidekiq_guide.md +0 -51
  194. data/examples/26_document_store.rb +0 -140
  195. data/examples/27_incident_response/incident_response.rb +0 -261
  196. data/examples/28_mcp_discovery.rb +0 -113
  197. data/examples/29_ractor_tools.rb +0 -240
  198. data/examples/30_ractor_network.rb +0 -252
  199. data/examples/31_launch_assessment.rb +0 -246
  200. data/examples/32_newsletter_reader.rb +0 -193
  201. data/examples/33_stock_generator.rb +0 -80
  202. data/examples/33_stock_predictor.rb +0 -327
  203. data/examples/34_agentskills.rb +0 -76
  204. data/examples/35_hooks.rb +0 -266
  205. data/examples/README.md +0 -533
  206. data/examples/common.rb +0 -148
  207. data/examples/prompts/assistant.md +0 -5
  208. data/examples/prompts/audit_trail.md +0 -8
  209. data/examples/prompts/billing.md +0 -10
  210. data/examples/prompts/classifier.md +0 -7
  211. data/examples/prompts/comedian.md +0 -6
  212. data/examples/prompts/comedy_critic.md +0 -10
  213. data/examples/prompts/configurable.md +0 -9
  214. data/examples/prompts/dispatcher.md +0 -12
  215. data/examples/prompts/entity_extractor.md +0 -14
  216. data/examples/prompts/escalation.md +0 -42
  217. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  218. data/examples/prompts/frontmatter_named_test.md +0 -5
  219. data/examples/prompts/frontmatter_tools_test.md +0 -6
  220. data/examples/prompts/general.md +0 -7
  221. data/examples/prompts/github_assistant.md +0 -14
  222. data/examples/prompts/helper.md +0 -4
  223. data/examples/prompts/incident_responder.md +0 -18
  224. data/examples/prompts/keyword_extractor.md +0 -11
  225. data/examples/prompts/llm_config_demo.md +0 -20
  226. data/examples/prompts/order_support.md +0 -35
  227. data/examples/prompts/os_advocate.md +0 -13
  228. data/examples/prompts/os_chief.md +0 -13
  229. data/examples/prompts/os_editor.md +0 -13
  230. data/examples/prompts/parameterized_main_test.md +0 -6
  231. data/examples/prompts/pii_redactor.md +0 -7
  232. data/examples/prompts/product_support.md +0 -37
  233. data/examples/prompts/runbook_protocol.md +0 -12
  234. data/examples/prompts/sentiment_analyzer.md +0 -12
  235. data/examples/prompts/skill_a_test.md +0 -4
  236. data/examples/prompts/skill_b_test.md +0 -4
  237. data/examples/prompts/skill_config_test.md +0 -5
  238. data/examples/prompts/skill_cycle_a_test.md +0 -6
  239. data/examples/prompts/skill_cycle_b_test.md +0 -6
  240. data/examples/prompts/skill_description_test.md +0 -4
  241. data/examples/prompts/skill_leaf_test.md +0 -4
  242. data/examples/prompts/skill_nested_test.md +0 -6
  243. data/examples/prompts/skill_refs_main_test.md +0 -6
  244. data/examples/prompts/skill_self_ref_test.md +0 -6
  245. data/examples/prompts/skill_with_mcp_test.md +0 -9
  246. data/examples/prompts/skill_with_params_test.md +0 -6
  247. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  248. data/examples/prompts/skill_with_tools_test.md +0 -6
  249. data/examples/prompts/sre_compliance.md +0 -10
  250. data/examples/prompts/structured_output.md +0 -7
  251. data/examples/prompts/synthesizer.md +0 -17
  252. data/examples/prompts/technical.md +0 -10
  253. data/examples/prompts/template_with_skills_test.md +0 -6
  254. data/examples/prompts/triage.md +0 -22
  255. data/examples/ruboruby.md +0 -423
  256. data/examples/xyzzy.rb +0 -97
  257. data/logfile +0 -8
  258. data/mkdocs.yml +0 -209
  259. data/robot_concurrency.md +0 -38
  260. data/simple_acp_review.md +0 -298
  261. data/temp.md +0 -6
  262. data/tool_manifest_plan.md +0 -155
@@ -1,261 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Example 27: Production Incident War Room
5
- #
6
- # A payment-service outage is in progress. Three SRE scouts investigate
7
- # different layers in parallel — database, network, and application. Each
8
- # scout writes findings to shared reactive memory and broadcasts a status
9
- # update to the war-room coordinator via TypedBus.
10
- #
11
- # Phase 5 infrastructure features demonstrated:
12
- #
13
- # REACTIVE MEMORY (IO.pipe Waiter — #13)
14
- # ──────────────────────────────────────────────────────────────
15
- # db_scout ─┐ memory[:db_finding] = "..." ─┐
16
- # net_scout ─┼ memory[:net_finding] = "..." ─┼→ IO.pipe signal
17
- # app_scout ─┘ memory[:app_finding] = "..." ─┘
18
- # ↓
19
- # commander.get(:db_finding, :net_finding, :app_finding, wait: 60)
20
- # wakes via IO.select on the pipe — no busy-wait, Async-safe
21
- #
22
- # BUS POLLER (serialized delivery — #14)
23
- # ──────────────────────────────────────────────────────────────
24
- # db_scout, net_scout, app_scout each send a bus message to :war_room.
25
- # If two scouts finish simultaneously, their deliveries queue in BusPoller
26
- # so war_room processes them one at a time — no re-entrancy, no dropped
27
- # messages, arrival order preserved.
28
- #
29
- # POLLER GROUPS (#15)
30
- # ──────────────────────────────────────────────────────────────
31
- # task :db_scout, poller_group: :investigation (fast, no LLM in final stage)
32
- # task :net_scout, poller_group: :investigation
33
- # task :app_scout, poller_group: :investigation
34
- # task :commander, poller_group: :command (expensive synthesis call)
35
- #
36
- # Usage:
37
- # bundle exec ruby examples/27_incident_response/incident_response.rb
38
-
39
- ENV["ROBOT_LAB_TEMPLATE_PATH"] ||= File.join(__dir__, "../prompts")
40
-
41
- require_relative "../common"
42
- require "fileutils"
43
-
44
- OUTPUT_DIR = File.join(__dir__, "output")
45
- FileUtils.mkdir_p(OUTPUT_DIR)
46
-
47
- # ── Scout ──────────────────────────────────────────────────────────────────
48
- #
49
- # Investigates one subsystem, stores a terse finding in shared memory,
50
- # and broadcasts a status line to the war-room via bus.
51
- #
52
- # NOTE: we bypass extract_run_context's delete() pattern and hold a direct
53
- # reference to shared memory — the same technique used in example 15 —
54
- # because parallel pipeline steps would otherwise lose the memory reference
55
- # after the first step runs.
56
-
57
- class SREScout < RobotLab::Robot
58
- attr_writer :shared_memory
59
-
60
- def initialize(name:, subsystem:, memory_key:, bus: nil)
61
- super(
62
- name: name,
63
- **llm_opts,
64
- system_prompt: "You are a senior SRE responding to a production outage. " \
65
- "Diagnose one infrastructure layer in 2 sentences: " \
66
- "first sentence is root cause, second is customer impact.",
67
- bus: bus
68
- )
69
- @subsystem = subsystem
70
- @memory_key = memory_key
71
- end
72
-
73
- def call(result)
74
- incident = extract_message(result)
75
-
76
- finding = run("Investigate the #{@subsystem} layer. #{incident}").reply.strip
77
-
78
- # Write to reactive memory — wakes the IO.pipe waiter in the commander
79
- if @shared_memory
80
- @shared_memory.current_writer = name
81
- @shared_memory.set(@memory_key, finding)
82
- end
83
-
84
- puts " [#{name}] #{finding[0..100]}#{"..." if finding.length > 100}"
85
-
86
- # Notify war room — BusPoller queues this if war_room is already processing
87
- send_message(to: :war_room, content: "[#{@subsystem}] #{finding}") if bus
88
-
89
- result.with_context(name.to_sym, finding).continue(finding)
90
- end
91
-
92
- private
93
-
94
- def extract_message(result)
95
- case result.value
96
- when Hash then result.value[:message].to_s
97
- when RobotLab::RobotResult then result.value.reply.to_s
98
- else result.value.to_s
99
- end
100
- end
101
- end
102
-
103
- # ── War Room ───────────────────────────────────────────────────────────────
104
- #
105
- # Receives scout status updates via TypedBus.
106
- # BusPoller serializes delivery: even if all three scouts finish at the
107
- # same instant their messages are processed one at a time and none is
108
- # dropped. Delivery order is preserved by the queue.
109
-
110
- class WarRoom < RobotLab::Robot
111
- attr_reader :updates
112
-
113
- def initialize(bus:)
114
- super(name: "war_room", **llm_opts, system_prompt: "SRE war-room coordinator.", bus: bus)
115
- @updates = []
116
-
117
- on_message do |msg|
118
- # BusPoller ensures this block is never re-entered for the same robot.
119
- # Simulate brief processing work so a second delivery would have to queue.
120
- @updates << msg.content
121
- puts " [war_room] Update ##{@updates.size} processed: #{msg.content[0..70]}..."
122
- end
123
- end
124
- end
125
-
126
- # ── Incident Commander ─────────────────────────────────────────────────────
127
- #
128
- # Waits until all three scout findings land in shared memory, then calls
129
- # the LLM once to synthesize an action plan.
130
- #
131
- # memory.get(:db_finding, :net_finding, :app_finding, wait: 60)
132
- # → internally each key uses an IO.pipe-backed Waiter
133
- # → IO.select wakes the call as soon as all three keys are written
134
- # → no busy-wait, no mutex spin, works cleanly with Async
135
-
136
- class IncidentCommander < RobotLab::Robot
137
- attr_writer :shared_memory
138
-
139
- FINDING_KEYS = %i[db_finding net_finding app_finding].freeze
140
-
141
- def call(result)
142
- puts " [commander] Blocking on reactive memory — waiting for all scout findings..."
143
-
144
- findings = collect_findings
145
-
146
- prompt = <<~PROMPT
147
- Three SRE scouts have reported their findings for a payment-service outage.
148
- Issue a concise incident action plan (3–5 bullet points) covering immediate
149
- mitigation, next investigation steps, and stakeholder communication.
150
-
151
- Database layer: #{findings[:db_finding] || "no data"}
152
- Network layer: #{findings[:net_finding] || "no data"}
153
- Application: #{findings[:app_finding] || "no data"}
154
- PROMPT
155
-
156
- report = run(prompt).reply.strip
157
-
158
- @shared_memory[:incident_report] = report
159
-
160
- path = File.join(OUTPUT_DIR, "incident_report.md")
161
- File.write(path, "# Incident Action Plan\n\n#{report}\n")
162
- puts " [commander] Action plan written to #{path}"
163
-
164
- result.with_context(:commander, report).continue(report)
165
- end
166
-
167
- private
168
-
169
- # Blocking multi-key read with a real degradation path.
170
- #
171
- # Memory#get(wait:) signals a timeout by RAISING RobotLab::AwaitTimeout — it
172
- # never returns a :timeout sentinel value. Rescue it and re-read without
173
- # wait:, which never blocks and simply omits keys that were never written,
174
- # so a slow scout degrades the report instead of killing the commander.
175
- def collect_findings
176
- @shared_memory.get(*FINDING_KEYS, wait: 60)
177
- rescue RobotLab::AwaitTimeout => e
178
- puts " [commander] WARNING: #{e.message}"
179
- partial = @shared_memory.get(*FINDING_KEYS)
180
- missing = FINDING_KEYS - partial.keys
181
- puts " [commander] Proceeding without: #{missing.join(', ')}" if missing.any?
182
- partial
183
- end
184
- end
185
-
186
- # ── Wire up ────────────────────────────────────────────────────────────────
187
-
188
- bus = TypedBus::MessageBus.new
189
-
190
- db_scout = SREScout.new(name: "db_scout", subsystem: "database", memory_key: :db_finding, bus: bus)
191
- net_scout = SREScout.new(name: "net_scout", subsystem: "network", memory_key: :net_finding, bus: bus)
192
- app_scout = SREScout.new(name: "app_scout", subsystem: "application", memory_key: :app_finding, bus: bus)
193
- war_room = WarRoom.new(bus: bus)
194
- commander = IncidentCommander.new(name: "commander", **llm_opts, system_prompt: "SRE incident commander.")
195
-
196
- # Build the investigation network
197
- # poller_group: labels are registered on the network's shared BusPoller.
198
- network = RobotLab.create_network(name: "incident_response") do
199
- task :db_scout, db_scout, depends_on: :none, poller_group: :investigation
200
- task :net_scout, net_scout, depends_on: :none, poller_group: :investigation
201
- task :app_scout, app_scout, depends_on: :none, poller_group: :investigation
202
- task :commander, commander, depends_on: [:db_scout, :net_scout, :app_scout], poller_group: :command
203
- end
204
-
205
- # Assign direct shared-memory references (avoids the extract_run_context
206
- # mutation problem with parallel pipeline steps — see example 15).
207
- shared_memory = network.memory
208
- [db_scout, net_scout, app_scout, commander].each do |r|
209
- r.shared_memory = shared_memory
210
- end
211
-
212
- # Subscribe to memory changes — fires as each scout writes its finding.
213
- # The callback runs asynchronously; the IO.pipe waiter in commander wakes
214
- # independently when the key is set.
215
- network.memory.subscribe(:db_finding, :net_finding, :app_finding) do |change|
216
- puts " [memory] :#{change.key} written by #{change.writer}"
217
- end
218
-
219
- # ── Run ────────────────────────────────────────────────────────────────────
220
-
221
- puts "=" * 65
222
- puts "Example 27: Production Incident War Room"
223
- puts " Phase 5 — BusPoller · Reactive Memory · Poller Groups"
224
- puts "=" * 65
225
- puts
226
- puts network.visualize
227
- puts
228
-
229
- puts "Poller groups declared on the network's tasks:"
230
- puts " :investigation — db_scout, net_scout, app_scout"
231
- puts " :command — commander"
232
- puts "(Network#task registers each group on the shared BusPoller; the poller"
233
- puts " itself is internal, so the groups are documented here rather than"
234
- puts " introspected.)"
235
- puts
236
-
237
- puts "INCIDENT: Payment service degraded — elevated error rates and timeouts"
238
- puts "-" * 65
239
- puts
240
-
241
- result = network.run(message: "Payment service is degraded: elevated HTTP 500 " \
242
- "error rates (~12%) and p99 latency spiked to 8s. " \
243
- "Investigate your assigned infrastructure layer.")
244
-
245
- puts
246
- puts "-" * 65
247
- puts "War-room updates received (BusPoller order):"
248
- war_room.updates.each.with_index(1) { |u, i| puts " #{i}. #{u.gsub(/\s+/, " ")[0..90]}" }
249
- puts
250
- puts "Reactive memory keys written by scouts:"
251
- %i[db_finding net_finding app_finding].each do |key|
252
- val = network.memory[key]
253
- puts " :#{key.to_s.ljust(14)} #{val&.gsub(/\s+/, " ")&.slice(0, 80)}..."
254
- end
255
- puts
256
- puts "Incident action plan (from #{OUTPUT_DIR}/incident_report.md):"
257
- puts "-" * 65
258
- report = network.memory[:incident_report].to_s
259
- puts report
260
- puts
261
- puts "=" * 65
@@ -1,113 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Example 28: MCP Server Discovery
5
- #
6
- # When a robot has many MCP servers configured, connecting to all of them
7
- # upfront is wasteful — some servers may be irrelevant to a particular query.
8
- #
9
- # MCP Server Discovery uses TF cosine similarity to select only the servers
10
- # semantically relevant to the user's query, then connects only those.
11
- #
12
- # == Key config
13
- #
14
- # robot = RobotLab.build(
15
- # provider: "ollama", model: "qwen3.6:latest",
16
- # mcp_discovery: true, # ← enables semantic filtering
17
- # mcp: [ ... ] # ← candidate servers, each with :description
18
- # )
19
- #
20
- # == Fallback behaviour
21
- #
22
- # All servers are connected unchanged when:
23
- # - No server has a :description field
24
- # - The classifier gem is unavailable
25
- # - The query is blank or nil
26
- # - No server scores at or above the threshold (0.05 by default)
27
- #
28
- # This demo exercises MCP::ServerDiscovery directly — no LLM calls needed.
29
- #
30
- # Usage:
31
- # bundle exec ruby examples/28_mcp_discovery.rb
32
-
33
- require_relative "common"
34
-
35
- # Three representative MCP server configurations
36
- SERVERS = [
37
- {
38
- name: "filesystem",
39
- description: "Read, write, and search local files and directories",
40
- transport: { type: "stdio", command: "mcp-server-filesystem" }
41
- },
42
- {
43
- name: "github",
44
- description: "GitHub repos, issues, pull requests, code search",
45
- transport: { type: "stdio", command: "mcp-server-github" }
46
- },
47
- {
48
- name: "brew",
49
- description: "Install, update, and manage macOS packages via Homebrew",
50
- transport: { type: "stdio", command: "mcp-server-brew" }
51
- }
52
- ].freeze
53
-
54
- def show_query(label, query)
55
- selected = RobotLab::MCP::ServerDiscovery.select(query, from: SERVERS)
56
- names = selected.map { |s| s[:name] }
57
-
58
- puts " #{label}"
59
- puts " Query : #{query.inspect}"
60
- puts " Match : #{names.inspect}"
61
- puts
62
- end
63
-
64
- banner "MCP Server Discovery"
65
- puts " Semantic server selection via TF cosine similarity"
66
- puts
67
- puts "Candidate servers:"
68
- SERVERS.each do |s|
69
- puts " #{s[:name].ljust(12)} #{s[:description]}"
70
- end
71
- puts
72
-
73
- section "Discovery Queries"
74
- show_query("File ops", "read my config file")
75
- show_query("Package mgmt", "install imagemagick via homebrew")
76
- show_query("Code review", "list open pull requests on my repo")
77
-
78
- section "Fallback Cases"
79
-
80
- # No description → all servers returned
81
- no_desc_servers = SERVERS.map { |s| s.except(:description) }
82
- result = RobotLab::MCP::ServerDiscovery.select("install imagemagick", from: no_desc_servers)
83
- puts " No descriptions : returns all (#{result.size} servers)"
84
-
85
- # Blank query → all servers returned
86
- result = RobotLab::MCP::ServerDiscovery.select("", from: SERVERS)
87
- puts " Blank query : returns all (#{result.size} servers)"
88
-
89
- # Very high threshold → no match → fallback to all
90
- result = RobotLab::MCP::ServerDiscovery.select("install imagemagick", from: SERVERS, threshold: 1.0)
91
- puts " High threshold : returns all (#{result.size} servers) — no match above 1.0"
92
-
93
- puts
94
- section "mcp_discovery: true on a Robot"
95
- show_code <<~RUBY
96
- robot = RobotLab.build(
97
- name: "assistant",
98
- provider: "ollama",
99
- model: "qwen3.6:latest",
100
- mcp_discovery: true,
101
- mcp: [
102
- { name: "filesystem", description: "Read, write...", transport: { } },
103
- { name: "github", description: "GitHub repos...", transport: { } },
104
- { name: "brew", description: "Install packages...", transport: { } }
105
- ]
106
- )
107
-
108
- # Only the :brew server is connected for this message.
109
- # mcp: :inherit and tools: :inherit are both required — run() defaults
110
- # both to :none, which connects no servers and sends no tools.
111
- robot.run("install imagemagick", mcp: :inherit, tools: :inherit)
112
- RUBY
113
- hr
@@ -1,240 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Example 29: Ractor-Safe CPU Tools
5
- #
6
- # Demonstrates Track 1 of RobotLab's Ractor parallelism: CPU-bound tools
7
- # that bypass Ruby's GVL by running inside a pool of Ractor workers.
8
- #
9
- # Concepts covered:
10
- # - ractor_safe true — opt a tool class into Ractor execution
11
- # - Inheritance — subclasses inherit ractor_safe automatically
12
- # - RactorBoundary.freeze_deep — deep-freeze nested data before
13
- # crossing the Ractor boundary
14
- # - RactorBoundaryError — raised for non-shareable values (Procs, IOs…)
15
- # - RactorWorkerPool — global pool, submit jobs, read frozen results
16
- # - ToolError — propagated when a tool raises inside a Ractor
17
- # - Parallel batch — many threads submitting concurrently vs sequentially
18
- #
19
- # Usage (no LLM traffic — pure CPU work):
20
- # bundle exec ruby examples/29_ractor_tools.rb
21
- #
22
- # Good to know:
23
- # Ractor dispatch has a fixed overhead of ~20 ms per job (reply-queue
24
- # Ractor creation + Ractor.make_shareable). Computation must dominate
25
- # that overhead for parallelism to win. HeavyDigestTool uses 500 000
26
- # SHA-256 rounds (~320 ms on modern hardware) so the 4-6× speedup is
27
- # clearly visible on a 6-core machine.
28
-
29
- require_relative "common"
30
- require "robot_lab/ractor"
31
- require "digest"
32
-
33
- # Always shut down the pool when the process exits.
34
- at_exit { RobotLab.shutdown_ractor_pool }
35
-
36
- # =============================================================================
37
- # Tool definitions
38
- # =============================================================================
39
-
40
- # Base class — declare ractor_safe once; all subclasses inherit it.
41
- class TextTool < RobotLab::Tool
42
- ractor_safe true
43
- end
44
-
45
- # Word and sentence statistics — pure computation, no shared state.
46
- class WordStatsTool < TextTool
47
- description "Count words, sentences, and average word length"
48
-
49
- param :text, type: :string, desc: "Text to analyze"
50
-
51
- def execute(text:)
52
- words = text.scan(/\b\w+\b/)
53
- sentences = [text.scan(/[.!?]+/).length, 1].max
54
- avg_len = words.empty? ? 0.0 : (words.sum(&:length).to_f / words.length).round(2)
55
-
56
- { words: words.length, sentences: sentences, avg_word_len: avg_len }.freeze
57
- end
58
- end
59
-
60
- # Words-per-sentence and long-word density (a simple readability proxy).
61
- class ReadabilityTool < TextTool
62
- description "Estimate words-per-sentence and long-word density"
63
-
64
- param :text, type: :string, desc: "Text to analyze"
65
-
66
- def execute(text:)
67
- words = text.scan(/\b\w+\b/)
68
- sentences = [text.scan(/[.!?]+/).length, 1].max
69
- long_words = words.count { |w| w.length > 6 }
70
- long_pct = words.empty? ? 0 : (long_words * 100 / words.length)
71
-
72
- {
73
- words: words.length,
74
- sentences: sentences,
75
- words_per_sentence: (words.length.to_f / sentences).round(1),
76
- long_word_pct: long_pct
77
- }.freeze
78
- end
79
- end
80
-
81
- # CPU-intensive: 500 000 SHA-256 rounds (~320 ms per job on modern hardware).
82
- # The overhead of reply-queue Ractor creation + make_shareable is ~20 ms per job
83
- # (constant), so computation must dwarf it to show a real speedup.
84
- class HeavyDigestTool < TextTool
85
- description "SHA-256 chain (500 000 rounds) — CPU-intensive, Ractor-safe"
86
-
87
- ROUNDS = 500_000
88
-
89
- param :text, type: :string, desc: "Seed text"
90
-
91
- def execute(text:)
92
- digest = text
93
- ROUNDS.times { digest = Digest::SHA256.hexdigest(digest) }
94
- digest[0..15].freeze
95
- end
96
- end
97
-
98
- # NOT Ractor-safe: @@hits is mutable class-level state.
99
- # Shown here purely as a contrast — do not submit this to the pool.
100
- class RequestCounterTool < RobotLab::Tool
101
- description "Word count plus a mutable global call counter (not Ractor-safe)"
102
-
103
- @@hits = 0 # mutable class variable — Ractor workers cannot access this
104
-
105
- param :text, type: :string, desc: "Text to count"
106
-
107
- def execute(text:)
108
- @@hits += 1
109
- "#{text.scan(/\b\w+\b/).length} words (total calls: #{@@hits})"
110
- end
111
- end
112
-
113
- # =============================================================================
114
- # Demo
115
- # =============================================================================
116
-
117
- banner "Ractor-Safe CPU Tools"
118
-
119
- DIVIDER = ("─" * 54).freeze
120
-
121
- SAMPLE_TEXTS = [
122
- "Ruby makes programmer happiness a first-class concern in language design.",
123
- "Ractors enable true CPU parallelism by isolating mutable state between actors.",
124
- "Every distributed system eventually becomes a consistency problem.",
125
- "The quick brown fox jumps over the lazy dog — a pangram.",
126
- "Machine learning transforms raw observations into actionable insight.",
127
- "Simplicity is the ultimate sophistication in software architecture."
128
- ].freeze
129
-
130
- # ── 1. ractor_safe? flags ─────────────────────────────────────
131
-
132
- puts "1. ractor_safe? flags"
133
- puts " #{DIVIDER}"
134
-
135
- {
136
- "WordStatsTool" => WordStatsTool,
137
- "ReadabilityTool" => ReadabilityTool,
138
- "HeavyDigestTool" => HeavyDigestTool,
139
- "RequestCounterTool" => RequestCounterTool
140
- }.each do |name, klass|
141
- mark = klass.ractor_safe ? "✓ ractor_safe (inherits from TextTool)" : "✗ NOT ractor_safe"
142
- puts " #{name.ljust(22)} #{mark}"
143
- end
144
- puts
145
-
146
- # ── 2. RactorBoundary.freeze_deep ─────────────────────────────
147
-
148
- puts "2. RactorBoundary.freeze_deep"
149
- puts " #{DIVIDER}"
150
-
151
- nested = { tags: ["ruby", "ractor"], meta: { version: 2 } }
152
- frozen = RobotLab::RactorBoundary.freeze_deep(nested)
153
-
154
- puts " Input frozen? #{nested.frozen?}"
155
- puts " Output frozen? #{frozen.frozen?}"
156
- puts " Inner :tags array frozen? #{frozen[:tags].frozen?}"
157
- puts " Inner :meta hash frozen? #{frozen[:meta].frozen?}"
158
- puts
159
-
160
- # Non-shareable objects (IO handles, Procs, ...) cannot cross a Ractor
161
- # boundary — freeze_deep rejects them immediately rather than failing later
162
- # inside a worker.
163
- require "stringio"
164
- begin
165
- RobotLab::RactorBoundary.freeze_deep(StringIO.new("I cannot be frozen"))
166
- rescue RobotLab::RactorBoundaryError => e
167
- puts " RactorBoundaryError on StringIO:"
168
- puts " #{e.message.split('.').first}."
169
- end
170
- puts
171
-
172
- # ── 3. Single pool submissions ─────────────────────────────────
173
-
174
- pool = RobotLab.ractor_pool
175
- puts "3. Worker pool (#{pool.size} Ractors — one per CPU core)"
176
- puts " #{DIVIDER}"
177
-
178
- sample = SAMPLE_TEXTS.first
179
-
180
- r = pool.submit("WordStatsTool", { text: sample })
181
- puts " WordStatsTool: #{r.inspect}"
182
-
183
- r = pool.submit("ReadabilityTool", { text: sample })
184
- puts " ReadabilityTool: words_per_sentence=#{r[:words_per_sentence]} long_word_pct=#{r[:long_word_pct]}%"
185
- puts
186
-
187
- # ── 4. ToolError propagation ───────────────────────────────────
188
-
189
- puts "4. ToolError propagation"
190
- puts " #{DIVIDER}"
191
- puts " Submitting nil as :text (WordStatsTool will call nil.scan — NoMethodError)"
192
- puts
193
-
194
- begin
195
- pool.submit("WordStatsTool", { text: nil })
196
- rescue RobotLab::ToolError => e
197
- puts " RobotLab::ToolError caught:"
198
- puts " #{e.message}"
199
- end
200
- puts
201
-
202
- # ── 5. Parallel batch vs sequential ───────────────────────────
203
-
204
- puts "5. Parallel batch — #{SAMPLE_TEXTS.length} jobs, each doing #{HeavyDigestTool::ROUNDS.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1_').reverse} SHA-256 rounds"
205
- puts " #{DIVIDER}"
206
-
207
- # Parallel: one Thread per job, all submitted simultaneously.
208
- t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
209
- threads = SAMPLE_TEXTS.map { |text| Thread.new { pool.submit("HeavyDigestTool", { text: text }) } }
210
- parallel_results = threads.map(&:value)
211
- parallel_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
212
-
213
- # Sequential: jobs submitted one-at-a-time from the main thread.
214
- t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
215
- SAMPLE_TEXTS.each { |text| pool.submit("HeavyDigestTool", { text: text }) }
216
- seq_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
217
-
218
- puts " Parallel (#{SAMPLE_TEXTS.length} threads → #{pool.size} Ractors): #{"%.3f" % parallel_time}s"
219
- puts " Sequential (1 thread → #{pool.size} Ractors): #{"%.3f" % seq_time}s"
220
-
221
- if seq_time > parallel_time * 1.2
222
- puts " Speedup: #{(seq_time / parallel_time).round(1)}×"
223
- else
224
- puts " Note: overhead (~20 ms/job for reply-queue Ractor + make_shareable)"
225
- puts " dominated this run. Increase ROUNDS further to widen the gap."
226
- end
227
-
228
- puts
229
- puts " First result (truncated digest): #{parallel_results.first}"
230
- puts
231
-
232
- # ── 6. Shutdown ────────────────────────────────────────────────
233
-
234
- puts "6. Shutdown"
235
- puts " #{DIVIDER}"
236
- RobotLab.shutdown_ractor_pool
237
- puts " Pool shut down cleanly (poison-pill × #{pool.size} workers)."
238
- puts
239
- hr
240
- puts "Example 29 complete."