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,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
- param :specialty, type: "string",
14
- desc: "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
- param :updated_criteria, type: "string",
50
- desc: "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