robot_lab 0.0.1 → 0.0.4

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 (145) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +9 -9
  3. data/.irbrc +6 -0
  4. data/CHANGELOG.md +90 -0
  5. data/README.md +203 -46
  6. data/Rakefile +70 -1
  7. data/docs/api/core/index.md +12 -0
  8. data/docs/api/core/robot.md +478 -130
  9. data/docs/api/core/tool.md +205 -209
  10. data/docs/api/history/active-record-adapter.md +174 -94
  11. data/docs/api/history/config.md +186 -93
  12. data/docs/api/history/index.md +57 -61
  13. data/docs/api/history/thread-manager.md +123 -73
  14. data/docs/api/mcp/client.md +119 -48
  15. data/docs/api/mcp/index.md +75 -60
  16. data/docs/api/mcp/server.md +120 -136
  17. data/docs/api/mcp/transports.md +172 -184
  18. data/docs/api/streaming/context.md +157 -74
  19. data/docs/api/streaming/events.md +114 -166
  20. data/docs/api/streaming/index.md +74 -72
  21. data/docs/architecture/core-concepts.md +361 -112
  22. data/docs/architecture/index.md +97 -59
  23. data/docs/architecture/message-flow.md +138 -129
  24. data/docs/architecture/network-orchestration.md +197 -50
  25. data/docs/architecture/robot-execution.md +199 -146
  26. data/docs/architecture/state-management.md +255 -187
  27. data/docs/concepts.md +312 -48
  28. data/docs/examples/basic-chat.md +89 -77
  29. data/docs/examples/index.md +222 -47
  30. data/docs/examples/mcp-server.md +207 -203
  31. data/docs/examples/multi-robot-network.md +129 -35
  32. data/docs/examples/rails-application.md +159 -160
  33. data/docs/examples/tool-usage.md +295 -204
  34. data/docs/getting-started/configuration.md +275 -162
  35. data/docs/getting-started/index.md +1 -1
  36. data/docs/getting-started/installation.md +22 -13
  37. data/docs/getting-started/quick-start.md +166 -121
  38. data/docs/guides/building-robots.md +417 -212
  39. data/docs/guides/creating-networks.md +94 -24
  40. data/docs/guides/mcp-integration.md +152 -113
  41. data/docs/guides/memory.md +220 -164
  42. data/docs/guides/streaming.md +80 -110
  43. data/docs/guides/using-tools.md +259 -212
  44. data/docs/index.md +50 -37
  45. data/examples/01_simple_robot.rb +6 -9
  46. data/examples/02_tools.rb +6 -9
  47. data/examples/03_network.rb +13 -14
  48. data/examples/04_mcp.rb +5 -8
  49. data/examples/05_streaming.rb +5 -8
  50. data/examples/06_prompt_templates.rb +42 -37
  51. data/examples/07_network_memory.rb +13 -14
  52. data/examples/08_llm_config.rb +140 -0
  53. data/examples/09_chaining.rb +223 -0
  54. data/examples/10_memory.rb +331 -0
  55. data/examples/11_network_introspection.rb +230 -0
  56. data/examples/12_message_bus.rb +74 -0
  57. data/examples/13_spawn.rb +90 -0
  58. data/examples/14_rusty_circuit/comic.rb +143 -0
  59. data/examples/14_rusty_circuit/display.rb +203 -0
  60. data/examples/14_rusty_circuit/heckler.rb +57 -0
  61. data/examples/14_rusty_circuit/open_mic.rb +121 -0
  62. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
  63. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
  64. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
  65. data/examples/14_rusty_circuit/scout.rb +173 -0
  66. data/examples/14_rusty_circuit/scout_notes.md +89 -0
  67. data/examples/14_rusty_circuit/show.log +234 -0
  68. data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
  69. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
  70. data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
  71. data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
  72. data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
  73. data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
  74. data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
  75. data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
  76. data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
  77. data/examples/15_memory_network_and_bus/output/memory.json +13 -0
  78. data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
  79. data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
  80. data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
  81. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
  82. data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
  83. data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
  84. data/examples/README.md +197 -0
  85. data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
  86. data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
  87. data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
  88. data/examples/prompts/comedian.md +6 -0
  89. data/examples/prompts/comedy_critic.md +10 -0
  90. data/examples/prompts/configurable.md +9 -0
  91. data/examples/prompts/dispatcher.md +12 -0
  92. data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
  93. data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
  94. data/examples/prompts/frontmatter_mcp_test.md +9 -0
  95. data/examples/prompts/frontmatter_named_test.md +5 -0
  96. data/examples/prompts/frontmatter_tools_test.md +6 -0
  97. data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
  98. data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
  99. data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
  100. data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
  101. data/examples/prompts/llm_config_demo.md +20 -0
  102. data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
  103. data/examples/prompts/os_advocate.md +13 -0
  104. data/examples/prompts/os_chief.md +13 -0
  105. data/examples/prompts/os_editor.md +13 -0
  106. data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
  107. data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
  108. data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
  109. data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
  110. data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
  111. data/lib/generators/robot_lab/templates/initializer.rb.tt +1 -1
  112. data/lib/robot_lab/adapters/openai.rb +2 -1
  113. data/lib/robot_lab/ask_user.rb +75 -0
  114. data/lib/robot_lab/config/defaults.yml +121 -0
  115. data/lib/robot_lab/config.rb +183 -0
  116. data/lib/robot_lab/error.rb +6 -0
  117. data/lib/robot_lab/mcp/client.rb +1 -1
  118. data/lib/robot_lab/memory.rb +2 -2
  119. data/lib/robot_lab/robot.rb +523 -249
  120. data/lib/robot_lab/robot_message.rb +44 -0
  121. data/lib/robot_lab/robot_result.rb +1 -0
  122. data/lib/robot_lab/robotic_model.rb +1 -1
  123. data/lib/robot_lab/streaming/context.rb +1 -1
  124. data/lib/robot_lab/tool.rb +108 -172
  125. data/lib/robot_lab/tool_config.rb +1 -1
  126. data/lib/robot_lab/tool_manifest.rb +2 -18
  127. data/lib/robot_lab/version.rb +1 -1
  128. data/lib/robot_lab.rb +66 -55
  129. metadata +107 -116
  130. data/examples/prompts/assistant/user.txt.erb +0 -1
  131. data/examples/prompts/billing/user.txt.erb +0 -1
  132. data/examples/prompts/classifier/user.txt.erb +0 -1
  133. data/examples/prompts/entity_extractor/user.txt.erb +0 -3
  134. data/examples/prompts/escalation/user.txt.erb +0 -34
  135. data/examples/prompts/general/user.txt.erb +0 -1
  136. data/examples/prompts/github_assistant/user.txt.erb +0 -1
  137. data/examples/prompts/helper/user.txt.erb +0 -1
  138. data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
  139. data/examples/prompts/order_support/user.txt.erb +0 -22
  140. data/examples/prompts/product_support/user.txt.erb +0 -32
  141. data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
  142. data/examples/prompts/synthesizer/user.txt.erb +0 -15
  143. data/examples/prompts/technical/user.txt.erb +0 -1
  144. data/examples/prompts/triage/user.txt.erb +0 -17
  145. data/lib/robot_lab/configuration.rb +0 -143
@@ -0,0 +1,203 @@
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
+ # ── Scout (file only) ───────────────────────────────────
73
+
74
+ def scout(round_num, notes)
75
+ @scout_file.puts "## Round #{round_num}\n\n#{notes}\n\n"
76
+ @scout_file.flush
77
+ log(" Scout [Round #{round_num}]: #{notes}")
78
+ end
79
+
80
+ def scout_analyst(name, text)
81
+ @scout_file.puts "### Analyst: #{name}\n\n#{text}\n\n"
82
+ @scout_file.flush
83
+ log(" [#{name}_analyst] #{text}")
84
+ end
85
+
86
+ def scout_criteria(text)
87
+ @scout_file.puts "### Criteria Refinement\n\n#{text}\n\n"
88
+ @scout_file.flush
89
+ log(" [refine_criteria] -> #{text}")
90
+ end
91
+
92
+ # ── Verdict (STDOUT green + file) ───────────────────────
93
+
94
+ def verdict(label, text)
95
+ puts
96
+ puts Rainbow(" #{label}:").green.bright
97
+ wrap(text, @term_width - 8).each do |line|
98
+ puts Rainbow(" #{line}").green
99
+ end
100
+ puts
101
+
102
+ @scout_file.puts "---\n\n## Final Verdict\n\n#{text}\n"
103
+ @scout_file.flush
104
+
105
+ log("\n #{label}:")
106
+ text.each_line { |line| log(" #{line.chomp}") }
107
+ log("")
108
+ end
109
+
110
+ # ── Chrome ──────────────────────────────────────────────
111
+
112
+ def banner(text)
113
+ puts
114
+ text.each_line { |line| puts Rainbow(line.chomp).bright }
115
+ puts
116
+
117
+ log("")
118
+ text.each_line { |line| log(line.chomp) }
119
+ log("")
120
+ end
121
+
122
+ def separator
123
+ puts Rainbow(" #{"─" * (@term_width - 4)}").darkgray
124
+ puts
125
+ log(" #{"─" * 56}")
126
+ log("")
127
+ end
128
+
129
+ def stats(text)
130
+ puts
131
+ text.each_line { |line| puts Rainbow(line.chomp).bright }
132
+
133
+ log("")
134
+ text.each_line { |line| log(line.chomp) }
135
+ end
136
+
137
+ def close
138
+ @scout_file.close unless @scout_file.closed?
139
+ @log_file&.close
140
+ end
141
+
142
+ private
143
+
144
+ def log(line)
145
+ return unless @log_file
146
+
147
+ @log_file.puts line
148
+ @log_file.flush
149
+ end
150
+
151
+ # Word-wrap text to fit within max_width display columns.
152
+ # Uses Unicode::DisplayWidth for correct CJK / emoji handling.
153
+ # Force-breaks any single word longer than max_width.
154
+ def wrap(text, max_width)
155
+ lines = []
156
+
157
+ text.each_line do |paragraph|
158
+ paragraph = paragraph.strip
159
+ next(lines << "") if paragraph.empty?
160
+
161
+ words = paragraph.split(/\s+/)
162
+ current = +""
163
+
164
+ words.each do |word|
165
+ word_w = Unicode::DisplayWidth.of(word)
166
+
167
+ # Force-break words wider than max_width
168
+ if word_w > max_width
169
+ unless current.empty?
170
+ lines << current
171
+ current = +""
172
+ end
173
+ chars = word.chars
174
+ buf = +""
175
+ chars.each do |ch|
176
+ if Unicode::DisplayWidth.of(buf + ch) > max_width
177
+ lines << buf
178
+ buf = +ch
179
+ else
180
+ buf << ch
181
+ end
182
+ end
183
+ current = buf
184
+ next
185
+ end
186
+
187
+ cur_w = Unicode::DisplayWidth.of(current)
188
+ if current.empty?
189
+ current = +word
190
+ elsif cur_w + 1 + word_w <= max_width
191
+ current << " " << word
192
+ else
193
+ lines << current
194
+ current = +word
195
+ end
196
+ end
197
+
198
+ lines << current unless current.empty?
199
+ end
200
+
201
+ lines
202
+ end
203
+ end
@@ -0,0 +1,57 @@
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
+ # Sends feedback directly to the comic's personal channel.
15
+ #
16
+ class Heckler < RobotLab::Robot
17
+ attr_reader :rounds, :won_over
18
+
19
+ def initialize(bus:, display:)
20
+ @rounds = 0
21
+ @won_over = false
22
+ @display = display
23
+
24
+ super(name: "heckler", template: :open_mic_heckler, bus: bus)
25
+
26
+ # Listen to the room for the comic's performances
27
+ @bus.subscribe(:room) do |delivery|
28
+ delivery.ack!
29
+ message = delivery.message
30
+ next unless message.from == "comic"
31
+ next if @rounds >= MAX_ROUNDS
32
+
33
+ @rounds += 1
34
+
35
+ verdict = run(
36
+ "The comedian just said: \"#{message.content}\"\n\n" \
37
+ "React however feels right — heckle, counter-joke, " \
38
+ "show respect, or stay silent."
39
+ ).reply.strip
40
+
41
+ # The heckler chose silence — no output, no feedback
42
+ if verdict.match?(/\[SILENCE\]/i)
43
+ next
44
+ end
45
+
46
+ @display.heckler("Heckler [Round #{@rounds}]", verdict)
47
+
48
+ if verdict.match?(/laugh|love|hilarious|brilliant|great/i)
49
+ @won_over = true
50
+ @display.heckler_note("(won over!)")
51
+ end
52
+
53
+ # Send feedback to comic's personal channel until the set is done
54
+ reply(message, verdict) if @rounds < MAX_ROUNDS
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,121 @@
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
+ # The scout uses a processing guard to serialize observations,
27
+ # preventing Async fiber interleaving from corrupting chat history.
28
+ #
29
+ # Style reinventions are injected into the next round's user prompt
30
+ # rather than modifying the chat's system messages, avoiding message
31
+ # ordering issues while achieving genuine self-modification.
32
+ #
33
+ # Usage:
34
+ # bundle exec ruby examples/14_rusty_circuit/open_mic.rb
35
+ # bundle exec ruby examples/14_rusty_circuit/open_mic.rb --log show.log
36
+
37
+ ENV["ROBOT_LAB_TEMPLATE_PATH"] ||= File.join(__dir__, "prompts")
38
+
39
+ require_relative "../../lib/robot_lab"
40
+ require_relative "display"
41
+
42
+ RubyLLM.configure { |c| c.logger = Logger.new(File::NULL) }
43
+
44
+ MAX_ROUNDS = 5
45
+
46
+ require_relative "comic"
47
+ require_relative "heckler"
48
+ require_relative "scout"
49
+
50
+ # ── The Show ─────────────────────────────────────────────────
51
+
52
+ log_path = nil
53
+ if (idx = ARGV.index("--log"))
54
+ log_path = ARGV[idx + 1]
55
+ abort "Usage: #{$0} [--log FILE]" unless log_path
56
+ end
57
+
58
+ bus = TypedBus::MessageBus.new
59
+ display = Display.new(
60
+ scout_path: File.join(__dir__, "scout_notes.md"),
61
+ log_path: log_path
62
+ )
63
+
64
+ # Shared room channel — the comic publishes performances here,
65
+ # and both the heckler and scout subscribe independently.
66
+ bus.add_channel(:room, type: RobotLab::RobotMessage)
67
+
68
+ comic = Comic.new(bus: bus, display: display)
69
+ heckler = Heckler.new(bus: bus, display: display)
70
+ scout = Scout.new(bus: bus, display: display)
71
+
72
+ display.banner(<<~BANNER)
73
+ ============================================================
74
+ THE RUSTY CIRCUIT — Tuesday Open Mic
75
+ Architecture Dominates Material
76
+ ============================================================
77
+
78
+ Cast:
79
+ Comic — observational humor, armed with self-modification tools
80
+ Heckler — three-year regular, high standards, zero patience
81
+ Scout — talent network, sitting in the back with a notebook
82
+ BANNER
83
+
84
+ display.separator
85
+
86
+ # The opening bit — comic performs, then enters the feedback loop
87
+ opening = comic.run(
88
+ "You just stepped on stage at The Rusty Circuit open mic. " \
89
+ "The crowd looks tough. Do your opening bit."
90
+ ).reply.strip
91
+
92
+ display.comic("Comic [Opening]", opening)
93
+
94
+ # Publish to room — both heckler and scout pick it up.
95
+ # The heckler reacts and sends feedback to the comic's personal
96
+ # channel, triggering the feedback loop:
97
+ # room → heckler → comic → room → heckler → comic → ...
98
+ # The loop terminates when the heckler stops replying (MAX_ROUNDS).
99
+ # The scout observes each round via :room with serialized processing.
100
+ comic.send_message(to: :room, content: "OPENING: #{opening}")
101
+
102
+ display.separator
103
+
104
+ # Final verdict from the talent scout
105
+ verdict = scout.run(scout.verdict_prompt).reply.strip
106
+
107
+ display.verdict("Scout [FINAL VERDICT]", verdict)
108
+
109
+ display.stats(<<~STATS)
110
+ ────────────────────────────────────────────────────────────
111
+ Show Stats:
112
+ Rounds performed: #{comic.round + 1} (opening + #{comic.round} rounds)
113
+ Style reinventions: #{comic.style_changes}
114
+ Coaches spawned: #{comic.coaches_spawned}
115
+ Analysts recruited: #{scout.analysts_spawned}
116
+ Heckler won over: #{heckler.won_over}
117
+ Total robots on bus: #{[comic, heckler, scout].size + comic.coaches_spawned + scout.analysts_spawned}
118
+
119
+ STATS
120
+
121
+ display.close
@@ -0,0 +1,20 @@
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.
@@ -0,0 +1,23 @@
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.
@@ -0,0 +1,20 @@
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]"
@@ -0,0 +1,173 @@
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
+ system_prompt:
26
+ "You are an expert #{specialty.tr('_', ' ')} analyst " \
27
+ "for stand-up comedy. You've studied the craft for decades. " \
28
+ "Analyze the performance notes you're given. Be concise " \
29
+ "and insightful. 2-3 sentences max."
30
+ )
31
+ end
32
+ analysis = analyst.run(robot.log.join("\n")).reply.strip
33
+ robot.display&.scout_analyst(specialty, analysis)
34
+ analysis
35
+ rescue => e
36
+ robot.display&.scout_analyst(specialty, "ERROR: #{e.message}")
37
+ "Analysis unavailable for #{specialty}. Rely on your own observations."
38
+ end
39
+ end
40
+
41
+ class RefineCriteria < RobotLab::Tool
42
+ description "Update your own evaluation criteria based on what " \
43
+ "you're observing. Use when you realize the most " \
44
+ "important qualities aren't what you initially expected. " \
45
+ "The update takes effect on your next evaluation."
46
+
47
+ param :updated_criteria, type: "string",
48
+ desc: "Your refined evaluation criteria and focus areas"
49
+
50
+ def execute(updated_criteria:)
51
+ robot.pending_criteria = updated_criteria
52
+ robot.display&.scout_criteria(updated_criteria)
53
+ "Criteria refinement accepted: #{updated_criteria}. " \
54
+ "Apply these updated criteria to all future evaluations."
55
+ end
56
+ end
57
+
58
+
59
+ # ── The Talent Scout ─────────────────────────────────────────
60
+ #
61
+ # Has two tools with side effects:
62
+ #
63
+ # recruit_analyst — spawns a specialist to analyze an aspect
64
+ # of the performance (dynamic creation)
65
+ # refine_criteria — queues a rewrite of the scout's own
66
+ # evaluation criteria (self-modification)
67
+ #
68
+ # The scout observes each round, accumulates notes, and spawns
69
+ # analysts when they see something worth examining closely.
70
+ #
71
+ # Subscribes to the :room channel to observe performances.
72
+ # Uses a processing guard to serialize run() calls — TypedBus
73
+ # delivers messages in concurrent Async fibers, and interleaved
74
+ # run() calls on the same @chat would corrupt tool_use/tool_result
75
+ # ordering in the Anthropic API.
76
+ #
77
+ class Scout < RobotLab::Robot
78
+ attr_accessor :log, :analysts_spawned, :pending_criteria, :display
79
+
80
+ def initialize(bus:, display:)
81
+ @log = []
82
+ @analysts_spawned = 0
83
+ @pending_criteria = nil
84
+ @processing = false
85
+ @observation_queue = []
86
+ @display = display
87
+
88
+ super(
89
+ name: "scout",
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
+ # Listen to the room for the comic's performances.
99
+ # Processing guard prevents concurrent run() calls on @chat —
100
+ # observations that arrive while processing are queued and
101
+ # drained sequentially after the current one completes.
102
+ @bus.subscribe(:room) do |delivery|
103
+ delivery.ack!
104
+ message = delivery.message
105
+ next unless message.from == "comic"
106
+
107
+ if @processing
108
+ @observation_queue << message.content.to_s
109
+ else
110
+ process_observation(message.content.to_s)
111
+ end
112
+ end
113
+ end
114
+
115
+ # Build the final verdict prompt with any pending criteria applied
116
+ def verdict_prompt
117
+ prompt = ""
118
+
119
+ if @pending_criteria
120
+ prompt += "CRITERIA UPDATE: #{@pending_criteria}. " \
121
+ "Apply these updated criteria to your final assessment.\n\n"
122
+ @pending_criteria = nil
123
+ end
124
+
125
+ prompt += "The show is over. Based on everything you've observed " \
126
+ "(#{@log.size} rounds), all your notes, and any analyst " \
127
+ "reports, write your final talent assessment. Should this " \
128
+ "comedian get a callback? Be specific about what worked, " \
129
+ "what didn't, and what potential you see."
130
+
131
+ prompt
132
+ end
133
+
134
+ private
135
+
136
+ def process_observation(content)
137
+ @processing = true
138
+
139
+ observe_and_note(content)
140
+
141
+ # Drain any observations that arrived while we were processing
142
+ while (queued = @observation_queue.shift)
143
+ observe_and_note(queued)
144
+ end
145
+
146
+ @processing = false
147
+ end
148
+
149
+ def observe_and_note(content)
150
+ @log << content
151
+
152
+ # Build the prompt, injecting any queued criteria refinement.
153
+ # This embeds self-modification in the user prompt rather than
154
+ # rewriting system messages, avoiding chat message ordering issues.
155
+ prompt = ""
156
+
157
+ if @pending_criteria
158
+ prompt += "CRITERIA UPDATE: #{@pending_criteria}. " \
159
+ "Apply these updated criteria to this and all future evaluations.\n\n"
160
+ @pending_criteria = nil
161
+ end
162
+
163
+ prompt += "You just observed: \"#{content}\"\n\n" \
164
+ "This is round #{@log.size} of the performance. " \
165
+ "Write your notes. If you've seen enough to identify " \
166
+ "patterns, consider recruiting an analyst or refining " \
167
+ "your evaluation criteria."
168
+
169
+ notes = run(prompt).reply.strip
170
+
171
+ @display.scout(@log.size, notes)
172
+ end
173
+ end