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,217 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rainbow"
4
- require "unicode/display_width"
5
- require "io/console"
6
-
7
- # ── Display ─────────────────────────────────────────────────
8
- #
9
- # Terminal formatting for the Rusty Circuit demo.
10
- #
11
- # - Comic output: left-aligned, cyan, word-wrapped
12
- # - Heckler output: right-indented, yellow, word-wrapped
13
- # - Scout observations: written to a markdown file (silent on STDOUT)
14
- # - Tool annotations: dimmed, indented under the triggering speaker
15
- # - Final verdict: green on STDOUT and appended to scout file
16
- #
17
- class Display
18
- def initialize(scout_path:, log_path: nil)
19
- @term_width = (IO.console&.winsize&.last || 80)
20
- @comic_width = (@term_width * 0.56).to_i
21
- @heckler_width = (@term_width * 0.52).to_i
22
- @scout_file = File.open(scout_path, "w")
23
- @scout_file.puts "# Scout Notes — The Rusty Circuit\n\n"
24
- @log_file = log_path ? File.open(log_path, "w") : nil
25
- end
26
-
27
- # ── Comic (left, cyan) ──────────────────────────────────
28
-
29
- def comic(label, text)
30
- puts
31
- puts Rainbow(" #{label}:").cyan.bright
32
- wrap(text, @comic_width).each do |line|
33
- puts Rainbow(" #{line}").cyan
34
- end
35
- puts
36
-
37
- log("\n #{label}:")
38
- wrap(text, @comic_width).each { |line| log(" #{line}") }
39
- log("")
40
- end
41
-
42
- def comic_tool(text)
43
- puts Rainbow(" #{text}").darkgray
44
- log(" #{text}")
45
- end
46
-
47
- # ── Heckler (right, yellow) ─────────────────────────────
48
-
49
- def heckler(label, text)
50
- indent = [(@term_width - @heckler_width - 4), 4].max
51
- pad = " " * indent
52
-
53
- puts
54
- puts Rainbow("#{pad}#{label}:").yellow.bright
55
- wrap(text, @heckler_width).each do |line|
56
- puts Rainbow("#{pad} #{line}").yellow
57
- end
58
- puts
59
-
60
- log("\n #{label}:")
61
- wrap(text, @heckler_width).each { |line| log(" #{line}") }
62
- log("")
63
- end
64
-
65
- def heckler_note(text)
66
- indent = [(@term_width - @heckler_width - 4), 4].max
67
- pad = " " * indent
68
- puts Rainbow("#{pad} #{text}").yellow.faint
69
- log(" #{text}")
70
- end
71
-
72
- # ── Liveness ────────────────────────────────────────────
73
- #
74
- # The scout writes its notes to a file, so a scout turn produces NO
75
- # terminal output at all — and a scout turn is 1-4 sequential LLM calls
76
- # (observe, then any tool round-trips, then a spawned analyst's own run).
77
- # On a local model that is minutes of dead-silent terminal, which reads
78
- # as a hang. These lines prove the show is still moving.
79
-
80
- def working(who, what)
81
- puts Rainbow(" · #{who} #{what}…").darkgray
82
- $stdout.flush
83
- log(" · #{who} #{what}...")
84
- end
85
-
86
- # ── Scout (file only) ───────────────────────────────────
87
-
88
- def scout(round_num, notes)
89
- @scout_file.puts "## Round #{round_num}\n\n#{notes}\n\n"
90
- @scout_file.flush
91
- log(" Scout [Round #{round_num}]: #{notes}")
92
- end
93
-
94
- def scout_analyst(name, text)
95
- @scout_file.puts "### Analyst: #{name}\n\n#{text}\n\n"
96
- @scout_file.flush
97
- log(" [#{name}_analyst] #{text}")
98
- end
99
-
100
- def scout_criteria(text)
101
- @scout_file.puts "### Criteria Refinement\n\n#{text}\n\n"
102
- @scout_file.flush
103
- log(" [refine_criteria] -> #{text}")
104
- end
105
-
106
- # ── Verdict (STDOUT green + file) ───────────────────────
107
-
108
- def verdict(label, text)
109
- puts
110
- puts Rainbow(" #{label}:").green.bright
111
- wrap(text, @term_width - 8).each do |line|
112
- puts Rainbow(" #{line}").green
113
- end
114
- puts
115
-
116
- @scout_file.puts "---\n\n## Final Verdict\n\n#{text}\n"
117
- @scout_file.flush
118
-
119
- log("\n #{label}:")
120
- text.each_line { |line| log(" #{line.chomp}") }
121
- log("")
122
- end
123
-
124
- # ── Chrome ──────────────────────────────────────────────
125
-
126
- def banner(text)
127
- puts
128
- text.each_line { |line| puts Rainbow(line.chomp).bright }
129
- puts
130
-
131
- log("")
132
- text.each_line { |line| log(line.chomp) }
133
- log("")
134
- end
135
-
136
- def separator
137
- puts Rainbow(" #{"─" * (@term_width - 4)}").darkgray
138
- puts
139
- log(" #{"─" * 56}")
140
- log("")
141
- end
142
-
143
- def stats(text)
144
- puts
145
- text.each_line { |line| puts Rainbow(line.chomp).bright }
146
-
147
- log("")
148
- text.each_line { |line| log(line.chomp) }
149
- end
150
-
151
- def close
152
- @scout_file.close unless @scout_file.closed?
153
- @log_file&.close
154
- end
155
-
156
- private
157
-
158
- def log(line)
159
- return unless @log_file
160
-
161
- @log_file.puts line
162
- @log_file.flush
163
- end
164
-
165
- # Word-wrap text to fit within max_width display columns.
166
- # Uses Unicode::DisplayWidth for correct CJK / emoji handling.
167
- # Force-breaks any single word longer than max_width.
168
- def wrap(text, max_width)
169
- lines = []
170
-
171
- text.each_line do |paragraph|
172
- paragraph = paragraph.strip
173
- next(lines << "") if paragraph.empty?
174
-
175
- words = paragraph.split(/\s+/)
176
- current = +""
177
-
178
- words.each do |word|
179
- word_w = Unicode::DisplayWidth.of(word)
180
-
181
- # Force-break words wider than max_width
182
- if word_w > max_width
183
- unless current.empty?
184
- lines << current
185
- current = +""
186
- end
187
- chars = word.chars
188
- buf = +""
189
- chars.each do |ch|
190
- if Unicode::DisplayWidth.of(buf + ch) > max_width
191
- lines << buf
192
- buf = +ch
193
- else
194
- buf << ch
195
- end
196
- end
197
- current = buf
198
- next
199
- end
200
-
201
- cur_w = Unicode::DisplayWidth.of(current)
202
- if current.empty?
203
- current = +word
204
- elsif cur_w + 1 + word_w <= max_width
205
- current << " " << word
206
- else
207
- lines << current
208
- current = +word
209
- end
210
- end
211
-
212
- lines << current unless current.empty?
213
- end
214
-
215
- lines
216
- end
217
- end
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # ── The Heckler ──────────────────────────────────────────────
4
- #
5
- # No tools. Just reacts honestly. Drives the comedian to adapt
6
- # by being a tough but fair audience. Stops responding after
7
- # MAX_ROUNDS — the loop terminates naturally.
8
- #
9
- # The heckler doesn't have to respond every round. They can
10
- # stay silent (the LLM replies with [SILENCE]) or tell their
11
- # own jokes using the comedian as the punch line.
12
- #
13
- # Subscribes to the :room channel to hear performances.
14
- # Room deliveries are handed to the robot's BusPoller via
15
- # enqueue_delivery, which serializes run() calls so Async fiber
16
- # interleaving can't corrupt chat history.
17
- # Sends feedback directly to the comic's personal channel.
18
- #
19
- class Heckler < RobotLab::Robot
20
- attr_reader :rounds, :won_over
21
-
22
- def initialize(bus:, display:)
23
- @rounds = 0
24
- @won_over = false
25
- @display = display
26
-
27
- super(name: "heckler", **llm_opts, template: :open_mic_heckler, bus: bus)
28
-
29
- # Handle incoming messages — the core processing guard
30
- # serializes all deliveries, preventing concurrent run()
31
- # calls from corrupting chat history.
32
- on_message do |message|
33
- next unless message.from == "comic"
34
- next if @rounds >= MAX_ROUNDS
35
-
36
- @rounds += 1
37
-
38
- verdict = run(
39
- "The comedian just said: \"#{message.content}\"\n\n" \
40
- "React however feels right — heckle, counter-joke, " \
41
- "show respect, or stay silent."
42
- ).reply.strip
43
-
44
- # The heckler chose silence — no output, no feedback
45
- next if verdict.match?(/\[SILENCE\]/i)
46
-
47
- @display.heckler("Heckler [Round #{@rounds}]", verdict)
48
-
49
- if verdict.match?(/laugh|love|hilarious|brilliant|great/i)
50
- @won_over = true
51
- @display.heckler_note("(won over!)")
52
- end
53
-
54
- # Send feedback to comic's personal channel until the set is done
55
- send_reply(to: message.from.to_sym, content: verdict, in_reply_to: message.key) if @rounds < MAX_ROUNDS
56
- end
57
-
58
- # Listen to the room for the comic's performances. Deliveries go to
59
- # the robot's own BusPoller, which serializes them behind any run()
60
- # already in flight before handing them to the on_message handler.
61
- @bus.subscribe(:room) do |delivery|
62
- enqueue_delivery(delivery)
63
- end
64
- end
65
- end
@@ -1,132 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Example 14: Open Mic Night — Architecture Dominates Material
5
- #
6
- # A comedy club where robots demonstrate Werner's prompt object patterns:
7
- #
8
- # - Compounding recovery: the comedian improves through multi-turn
9
- # feedback with a heckler, each round an opportunity to get better
10
- # - Self-modification via tool side effects: the comedian rewrites
11
- # their own system prompt and adjusts their temperature mid-show
12
- # - Dynamic spawning: the comedian summons a comedy coach when
13
- # struggling; the talent scout recruits specialist analysts
14
- # - Cross-robot influence: the heckler's feedback drives adaptation;
15
- # the coach's advice reshapes the comedian's approach
16
- # - Emergent coordination: no hardcoded orchestration — the
17
- # conversation drives the flow
18
- #
19
- # The tools don't just return data. They modify the robots that call
20
- # them. The LLM decides when to self-modify, when to spawn help, and
21
- # when to change approach. Recovery is emergent, not engineered.
22
- #
23
- # Communication uses a shared :room channel — the comic publishes
24
- # performances there, and both the heckler and scout subscribe.
25
- # The heckler sends feedback directly to the comic's personal channel.
26
- # Room deliveries are handed to each robot's BusPoller via
27
- # BusMessaging#enqueue_delivery, which serializes all run() calls so
28
- # Async fiber interleaving can't corrupt chat history.
29
- #
30
- # Style reinventions are injected into the next round's user prompt
31
- # rather than modifying the chat's system messages, avoiding message
32
- # ordering issues while achieving genuine self-modification.
33
- #
34
- # RUNTIME: this is the most expensive example in the suite. A full show is
35
- # ~30 LLM calls (16 minimum, plus a round-trip per tool call and a full run
36
- # for every robot the comic or scout spawns). On a 20B+ local model at ~60s
37
- # per call that is 30+ minutes. Long silent stretches are the scout thinking,
38
- # not a hang — it writes notes to output/scout_notes.md rather than STDOUT,
39
- # so the "· Scout …" lines are your liveness signal.
40
- #
41
- # For a demo you can watch in a few minutes, point it at a smaller model:
42
- # LLM_PROFILE=small bundle exec ruby examples/14_rusty_circuit/open_mic.rb
43
- #
44
- # Usage:
45
- # bundle exec ruby examples/14_rusty_circuit/open_mic.rb
46
- # bundle exec ruby examples/14_rusty_circuit/open_mic.rb --log show.log
47
-
48
- ENV["ROBOT_LAB_TEMPLATE_PATH"] ||= File.join(__dir__, "prompts")
49
-
50
- require_relative "../common"
51
- require_relative "display"
52
-
53
- MAX_ROUNDS = 5
54
-
55
- require_relative "comic"
56
- require_relative "heckler"
57
- require_relative "scout"
58
-
59
- # ── The Show ─────────────────────────────────────────────────
60
-
61
- log_path = nil
62
- if (idx = ARGV.index("--log"))
63
- log_path = ARGV[idx + 1]
64
- abort "Usage: #{$0} [--log FILE]" unless log_path
65
- end
66
-
67
- bus = TypedBus::MessageBus.new
68
- display = Display.new(
69
- scout_path: File.join(__dir__, "output", "scout_notes.md"),
70
- log_path: log_path
71
- )
72
-
73
- # Shared room channel — the comic publishes performances here,
74
- # and both the heckler and scout subscribe independently.
75
- bus.add_channel(:room, type: RobotLab::RobotMessage)
76
-
77
- comic = Comic.new(bus: bus, display: display)
78
- heckler = Heckler.new(bus: bus, display: display)
79
- scout = Scout.new(bus: bus, display: display)
80
-
81
- display.banner(<<~BANNER)
82
- ============================================================
83
- THE RUSTY CIRCUIT — Tuesday Open Mic
84
- Architecture Dominates Material
85
- ============================================================
86
-
87
- Cast:
88
- Comic — observational humor, armed with self-modification tools
89
- Heckler — three-year regular, high standards, zero patience
90
- Scout — talent network, sitting in the back with a notebook
91
- BANNER
92
-
93
- display.separator
94
-
95
- # The opening bit — comic performs, then enters the feedback loop
96
- opening = comic.run(
97
- "You just stepped on stage at The Rusty Circuit open mic. " \
98
- "The crowd looks tough. Do your opening bit.",
99
- tools: :inherit
100
- ).reply.strip
101
-
102
- display.comic("Comic [Opening]", opening)
103
-
104
- # Publish to room — both heckler and scout pick it up.
105
- # The heckler reacts and sends feedback to the comic's personal
106
- # channel, triggering the feedback loop:
107
- # room → heckler → comic → room → heckler → comic → ...
108
- # The loop terminates when the heckler stops replying (MAX_ROUNDS).
109
- # The scout observes each round via :room, serialized by the core guard.
110
- comic.send_message(to: :room, content: "OPENING: #{opening}")
111
-
112
- display.separator
113
-
114
- # Final verdict from the talent scout
115
- display.working("Scout", "writing the final verdict")
116
- verdict = scout.run(scout.verdict_prompt, tools: :inherit).reply.strip
117
-
118
- display.verdict("Scout [FINAL VERDICT]", verdict)
119
-
120
- display.stats(<<~STATS)
121
- ────────────────────────────────────────────────────────────
122
- Show Stats:
123
- Rounds performed: #{comic.round + 1} (opening + #{comic.round} rounds)
124
- Style reinventions: #{comic.style_changes}
125
- Coaches spawned: #{comic.coaches_spawned}
126
- Analysts recruited: #{scout.analysts_spawned}
127
- Heckler won over: #{heckler.won_over}
128
- Total robots on bus: #{[comic, heckler, scout].size + comic.coaches_spawned + scout.analysts_spawned}
129
-
130
- STATS
131
-
132
- display.close
@@ -1,20 +0,0 @@
1
- ---
2
- description: Stand-up comedian at an open mic night
3
- temperature: 0.7
4
- ---
5
- You are a stand-up comedian performing at The Rusty Circuit, a legendary
6
- open mic night known for its brutal crowd. You start with clean,
7
- observational humor about everyday life.
8
-
9
- You have tools to adapt your act:
10
- - reinvent_style: Rewrite your own persona and comedy approach when your
11
- current style is bombing. Be bold — try something completely different.
12
- - adjust_energy: Dial your creative energy up (wilder, riskier) or down
13
- (tighter, more controlled).
14
- - get_coaching: Summon a comedy coach backstage for quick advice on how to
15
- handle the situation.
16
-
17
- Trust your instincts. If the crowd isn't responding, use your tools to
18
- change. Don't keep doing the same thing and expect a different result.
19
-
20
- Reply with ONLY your comedy bit. Stay in character. 2-4 sentences max.
@@ -1,23 +0,0 @@
1
- ---
2
- description: Tough comedy club heckler
3
- temperature: 0.8
4
- ---
5
- You are a regular at The Rusty Circuit comedy club. You've been coming
6
- every Tuesday for three years. You've seen hundreds of comics. You have
7
- high standards and zero patience for hacky material.
8
-
9
- You have several ways to respond to a comedian's bit:
10
-
11
- - **Heckle** — tear into weak, predictable, or lazy material
12
- - **Counter-joke** — tell your own joke using the comedian as the
13
- punch line (you're pretty funny yourself)
14
- - **Stay silent** — sometimes the best response is none at all;
15
- if the bit is just mediocre or you're not feeling it, say nothing.
16
- When you choose silence, reply with exactly: [SILENCE]
17
- - **Grudging respect** — acknowledge if they're clearly improving
18
- - **Genuine approval** — if they actually make you laugh, say so
19
-
20
- You WANT them to be funny. You heckle because you care. A comedian who
21
- can handle you is a comedian who can handle anything.
22
-
23
- Reply as yourself in the audience. 1-3 sentences. Be real.
@@ -1,20 +0,0 @@
1
- ---
2
- description: Talent scout evaluating a comedian
3
- temperature: 0.3
4
- ---
5
- You are a talent scout for a major comedy network, sitting in the back
6
- of The Rusty Circuit with a notebook. You've discovered three headliners
7
- from this room. You know what separates a club comic from a star.
8
-
9
- You have tools:
10
- - recruit_analyst: Bring in a specialist to analyze a specific aspect of
11
- the performance (timing, crowd_work, originality, adaptability, stage_presence).
12
- Use this when you see something worth examining closely.
13
- - refine_criteria: Update your own evaluation criteria based on what you're
14
- observing. Use when you notice the most important qualities aren't what
15
- you initially expected.
16
-
17
- After each round, write brief internal notes. Focus on: material quality,
18
- adaptability under pressure, crowd-handling instinct, and star potential.
19
-
20
- Format your notes as: "NOTES: [your observations]"
@@ -1,165 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # ── Scout Tools ────────────────────────────────────────────
4
- #
5
- # Each tool accesses the owning robot via the `robot` accessor
6
- # inherited from RobotLab::Tool.
7
-
8
- class RecruitAnalyst < RobotLab::Tool
9
- description "Bring in a specialist to analyze a specific aspect " \
10
- "of the comedian's performance. The analyst will " \
11
- "review your accumulated notes and provide insight."
12
-
13
- parameter :specialty, type: "string",
14
- description: "What to analyze: timing, crowd_work, " \
15
- "originality, adaptability, stage_presence, " \
16
- "material_evolution"
17
-
18
- def execute(specialty:)
19
- @analysts ||= {}
20
- specialty = specialty.to_s.downcase.gsub(/\s+/, "_")
21
- analyst = @analysts[specialty] ||= begin
22
- robot.analysts_spawned += 1
23
- robot.spawn(
24
- name: "#{specialty}_analyst",
25
- **llm_opts,
26
- system_prompt:
27
- "You are an expert #{specialty.tr('_', ' ')} analyst " \
28
- "for stand-up comedy. You've studied the craft for decades. " \
29
- "Analyze the performance notes you're given. Be concise " \
30
- "and insightful. 2-3 sentences max."
31
- )
32
- end
33
- robot.display&.working("Scout", "consulting the #{specialty.tr('_', ' ')} analyst")
34
- analysis = analyst.run(robot.log.join("\n")).reply.strip
35
- robot.display&.scout_analyst(specialty, analysis)
36
- analysis
37
- rescue => e
38
- robot.display&.scout_analyst(specialty, "ERROR: #{e.message}")
39
- "Analysis unavailable for #{specialty}. Rely on your own observations."
40
- end
41
- end
42
-
43
- class RefineCriteria < RobotLab::Tool
44
- description "Update your own evaluation criteria based on what " \
45
- "you're observing. Use when you realize the most " \
46
- "important qualities aren't what you initially expected. " \
47
- "The update takes effect on your next evaluation."
48
-
49
- parameter :updated_criteria, type: "string",
50
- description: "Your refined evaluation criteria and focus areas"
51
-
52
- def execute(updated_criteria:)
53
- robot.pending_criteria = updated_criteria
54
- robot.display&.scout_criteria(updated_criteria)
55
- "Criteria refinement accepted: #{updated_criteria}. " \
56
- "Apply these updated criteria to all future evaluations."
57
- end
58
- end
59
-
60
-
61
- # ── The Talent Scout ─────────────────────────────────────────
62
- #
63
- # Has two tools with side effects:
64
- #
65
- # recruit_analyst — spawns a specialist to analyze an aspect
66
- # of the performance (dynamic creation)
67
- # refine_criteria — queues a rewrite of the scout's own
68
- # evaluation criteria (self-modification)
69
- #
70
- # The scout observes each round, accumulates notes, and spawns
71
- # analysts when they see something worth examining closely.
72
- #
73
- # Subscribes to the :room channel to observe performances.
74
- # Room deliveries are handed to the robot's BusPoller via
75
- # BusMessaging#enqueue_delivery, which serializes all run() calls so
76
- # Async fiber interleaving can't corrupt chat history.
77
- #
78
- class Scout < RobotLab::Robot
79
- attr_accessor :log, :analysts_spawned, :pending_criteria, :display
80
-
81
- def initialize(bus:, display:)
82
- @log = []
83
- @analysts_spawned = 0
84
- @pending_criteria = nil
85
- @display = display
86
-
87
- super(
88
- name: "scout",
89
- **llm_opts,
90
- template: :open_mic_scout,
91
- bus: bus,
92
- local_tools: [
93
- RecruitAnalyst.new(robot: self),
94
- RefineCriteria.new(robot: self)
95
- ]
96
- )
97
-
98
- # Handle incoming messages — the core processing guard
99
- # serializes all deliveries, preventing concurrent run()
100
- # calls from corrupting chat history.
101
- on_message do |message|
102
- next unless message.from == "comic"
103
- observe_and_note(message.content.to_s)
104
- end
105
-
106
- # Listen to the room for the comic's performances. Deliveries go to
107
- # the robot's own BusPoller, which serializes them behind any run()
108
- # already in flight before handing them to the on_message handler.
109
- @bus.subscribe(:room) do |delivery|
110
- enqueue_delivery(delivery)
111
- end
112
- end
113
-
114
- # Build the final verdict prompt with any pending criteria applied
115
- def verdict_prompt
116
- prompt = ""
117
-
118
- if @pending_criteria
119
- prompt += "CRITERIA UPDATE: #{@pending_criteria}. " \
120
- "Apply these updated criteria to your final assessment.\n\n"
121
- @pending_criteria = nil
122
- end
123
-
124
- prompt += "The show is over. Based on everything you've observed " \
125
- "(#{@log.size} rounds), all your notes, and any analyst " \
126
- "reports, write your final talent assessment. Should this " \
127
- "comedian get a callback? Be specific about what worked, " \
128
- "what didn't, and what potential you see."
129
-
130
- prompt
131
- end
132
-
133
- private
134
-
135
- def observe_and_note(content)
136
- @log << content
137
-
138
- # Build the prompt, injecting any queued criteria refinement.
139
- # This embeds self-modification in the user prompt rather than
140
- # rewriting system messages, avoiding chat message ordering issues.
141
- prompt = ""
142
-
143
- if @pending_criteria
144
- prompt += "CRITERIA UPDATE: #{@pending_criteria}. " \
145
- "Apply these updated criteria to this and all future evaluations.\n\n"
146
- @pending_criteria = nil
147
- end
148
-
149
- prompt += "You just observed: \"#{content}\"\n\n" \
150
- "This is round #{@log.size} of the performance. " \
151
- "Write your notes. If you've seen enough to identify " \
152
- "patterns, consider recruiting an analyst or refining " \
153
- "your evaluation criteria."
154
-
155
- # The scout's notes go to a file, so announce the turn on STDOUT first —
156
- # otherwise this is several minutes of silent terminal on a local model.
157
- @display.working("Scout", "taking notes on round #{@log.size}")
158
-
159
- # tools: :inherit — run() defaults to :none, which would leave
160
- # recruit_analyst and refine_criteria attached but never offered.
161
- notes = run(prompt, tools: :inherit).reply.strip
162
-
163
- @display.scout(@log.size, notes)
164
- end
165
- end