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.
- checksums.yaml +4 -4
- data/.github/workflows/deploy-github-pages.yml +9 -9
- data/.irbrc +6 -0
- data/CHANGELOG.md +90 -0
- data/README.md +203 -46
- data/Rakefile +70 -1
- data/docs/api/core/index.md +12 -0
- data/docs/api/core/robot.md +478 -130
- data/docs/api/core/tool.md +205 -209
- data/docs/api/history/active-record-adapter.md +174 -94
- data/docs/api/history/config.md +186 -93
- data/docs/api/history/index.md +57 -61
- data/docs/api/history/thread-manager.md +123 -73
- data/docs/api/mcp/client.md +119 -48
- data/docs/api/mcp/index.md +75 -60
- data/docs/api/mcp/server.md +120 -136
- data/docs/api/mcp/transports.md +172 -184
- data/docs/api/streaming/context.md +157 -74
- data/docs/api/streaming/events.md +114 -166
- data/docs/api/streaming/index.md +74 -72
- data/docs/architecture/core-concepts.md +361 -112
- data/docs/architecture/index.md +97 -59
- data/docs/architecture/message-flow.md +138 -129
- data/docs/architecture/network-orchestration.md +197 -50
- data/docs/architecture/robot-execution.md +199 -146
- data/docs/architecture/state-management.md +255 -187
- data/docs/concepts.md +312 -48
- data/docs/examples/basic-chat.md +89 -77
- data/docs/examples/index.md +222 -47
- data/docs/examples/mcp-server.md +207 -203
- data/docs/examples/multi-robot-network.md +129 -35
- data/docs/examples/rails-application.md +159 -160
- data/docs/examples/tool-usage.md +295 -204
- data/docs/getting-started/configuration.md +275 -162
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/installation.md +22 -13
- data/docs/getting-started/quick-start.md +166 -121
- data/docs/guides/building-robots.md +417 -212
- data/docs/guides/creating-networks.md +94 -24
- data/docs/guides/mcp-integration.md +152 -113
- data/docs/guides/memory.md +220 -164
- data/docs/guides/streaming.md +80 -110
- data/docs/guides/using-tools.md +259 -212
- data/docs/index.md +50 -37
- data/examples/01_simple_robot.rb +6 -9
- data/examples/02_tools.rb +6 -9
- data/examples/03_network.rb +13 -14
- data/examples/04_mcp.rb +5 -8
- data/examples/05_streaming.rb +5 -8
- data/examples/06_prompt_templates.rb +42 -37
- data/examples/07_network_memory.rb +13 -14
- data/examples/08_llm_config.rb +140 -0
- data/examples/09_chaining.rb +223 -0
- data/examples/10_memory.rb +331 -0
- data/examples/11_network_introspection.rb +230 -0
- data/examples/12_message_bus.rb +74 -0
- data/examples/13_spawn.rb +90 -0
- data/examples/14_rusty_circuit/comic.rb +143 -0
- data/examples/14_rusty_circuit/display.rb +203 -0
- data/examples/14_rusty_circuit/heckler.rb +57 -0
- data/examples/14_rusty_circuit/open_mic.rb +121 -0
- data/examples/14_rusty_circuit/prompts/open_mic_comic.md +20 -0
- data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +23 -0
- data/examples/14_rusty_circuit/prompts/open_mic_scout.md +20 -0
- data/examples/14_rusty_circuit/scout.rb +173 -0
- data/examples/14_rusty_circuit/scout_notes.md +89 -0
- data/examples/14_rusty_circuit/show.log +234 -0
- data/examples/15_memory_network_and_bus/editor_in_chief.rb +24 -0
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +206 -0
- data/examples/15_memory_network_and_bus/linux_writer.rb +80 -0
- data/examples/15_memory_network_and_bus/os_editor.rb +46 -0
- data/examples/15_memory_network_and_bus/os_writer.rb +46 -0
- data/examples/15_memory_network_and_bus/output/combined_article.md +13 -0
- data/examples/15_memory_network_and_bus/output/final_article.md +15 -0
- data/examples/15_memory_network_and_bus/output/linux_draft.md +5 -0
- data/examples/15_memory_network_and_bus/output/mac_draft.md +7 -0
- data/examples/15_memory_network_and_bus/output/memory.json +13 -0
- data/examples/15_memory_network_and_bus/output/revision_1.md +19 -0
- data/examples/15_memory_network_and_bus/output/revision_2.md +15 -0
- data/examples/15_memory_network_and_bus/output/windows_draft.md +7 -0
- data/examples/15_memory_network_and_bus/prompts/os_advocate.md +13 -0
- data/examples/15_memory_network_and_bus/prompts/os_chief.md +13 -0
- data/examples/15_memory_network_and_bus/prompts/os_editor.md +13 -0
- data/examples/README.md +197 -0
- data/examples/prompts/{assistant/system.txt.erb → assistant.md} +3 -0
- data/examples/prompts/{billing/system.txt.erb → billing.md} +3 -0
- data/examples/prompts/{classifier/system.txt.erb → classifier.md} +3 -0
- data/examples/prompts/comedian.md +6 -0
- data/examples/prompts/comedy_critic.md +10 -0
- data/examples/prompts/configurable.md +9 -0
- data/examples/prompts/dispatcher.md +12 -0
- data/examples/prompts/{entity_extractor/system.txt.erb → entity_extractor.md} +3 -0
- data/examples/prompts/{escalation/system.txt.erb → escalation.md} +7 -0
- data/examples/prompts/frontmatter_mcp_test.md +9 -0
- data/examples/prompts/frontmatter_named_test.md +5 -0
- data/examples/prompts/frontmatter_tools_test.md +6 -0
- data/examples/prompts/{general/system.txt.erb → general.md} +3 -0
- data/examples/prompts/{github_assistant/system.txt.erb → github_assistant.md} +8 -0
- data/examples/prompts/{helper/system.txt.erb → helper.md} +3 -0
- data/examples/prompts/{keyword_extractor/system.txt.erb → keyword_extractor.md} +3 -0
- data/examples/prompts/llm_config_demo.md +20 -0
- data/examples/prompts/{order_support/system.txt.erb → order_support.md} +8 -0
- data/examples/prompts/os_advocate.md +13 -0
- data/examples/prompts/os_chief.md +13 -0
- data/examples/prompts/os_editor.md +13 -0
- data/examples/prompts/{product_support/system.txt.erb → product_support.md} +7 -0
- data/examples/prompts/{sentiment_analyzer/system.txt.erb → sentiment_analyzer.md} +3 -0
- data/examples/prompts/{synthesizer/system.txt.erb → synthesizer.md} +3 -0
- data/examples/prompts/{technical/system.txt.erb → technical.md} +3 -0
- data/examples/prompts/{triage/system.txt.erb → triage.md} +6 -0
- data/lib/generators/robot_lab/templates/initializer.rb.tt +1 -1
- data/lib/robot_lab/adapters/openai.rb +2 -1
- data/lib/robot_lab/ask_user.rb +75 -0
- data/lib/robot_lab/config/defaults.yml +121 -0
- data/lib/robot_lab/config.rb +183 -0
- data/lib/robot_lab/error.rb +6 -0
- data/lib/robot_lab/mcp/client.rb +1 -1
- data/lib/robot_lab/memory.rb +2 -2
- data/lib/robot_lab/robot.rb +523 -249
- data/lib/robot_lab/robot_message.rb +44 -0
- data/lib/robot_lab/robot_result.rb +1 -0
- data/lib/robot_lab/robotic_model.rb +1 -1
- data/lib/robot_lab/streaming/context.rb +1 -1
- data/lib/robot_lab/tool.rb +108 -172
- data/lib/robot_lab/tool_config.rb +1 -1
- data/lib/robot_lab/tool_manifest.rb +2 -18
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +66 -55
- metadata +107 -116
- data/examples/prompts/assistant/user.txt.erb +0 -1
- data/examples/prompts/billing/user.txt.erb +0 -1
- data/examples/prompts/classifier/user.txt.erb +0 -1
- data/examples/prompts/entity_extractor/user.txt.erb +0 -3
- data/examples/prompts/escalation/user.txt.erb +0 -34
- data/examples/prompts/general/user.txt.erb +0 -1
- data/examples/prompts/github_assistant/user.txt.erb +0 -1
- data/examples/prompts/helper/user.txt.erb +0 -1
- data/examples/prompts/keyword_extractor/user.txt.erb +0 -3
- data/examples/prompts/order_support/user.txt.erb +0 -22
- data/examples/prompts/product_support/user.txt.erb +0 -32
- data/examples/prompts/sentiment_analyzer/user.txt.erb +0 -3
- data/examples/prompts/synthesizer/user.txt.erb +0 -15
- data/examples/prompts/technical/user.txt.erb +0 -1
- data/examples/prompts/triage/user.txt.erb +0 -17
- 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
|