robot_lab 0.0.1 → 0.0.6
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 +140 -0
- data/README.md +263 -48
- data/Rakefile +71 -1
- data/docs/api/core/index.md +53 -46
- data/docs/api/core/memory.md +200 -154
- data/docs/api/core/network.md +13 -3
- data/docs/api/core/robot.md +490 -130
- data/docs/api/core/state.md +55 -73
- data/docs/api/core/tool.md +205 -209
- data/docs/api/index.md +7 -28
- 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/messages/index.md +35 -20
- data/docs/api/messages/text-message.md +67 -21
- data/docs/api/messages/tool-call-message.md +80 -41
- data/docs/api/messages/tool-result-message.md +119 -50
- data/docs/api/messages/user-message.md +48 -24
- 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 +360 -116
- 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 +311 -49
- 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 +347 -154
- 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 +418 -212
- data/docs/guides/creating-networks.md +143 -24
- data/docs/guides/index.md +0 -5
- data/docs/guides/mcp-integration.md +152 -113
- data/docs/guides/memory.md +220 -164
- data/docs/guides/rails-integration.md +244 -162
- data/docs/guides/streaming.md +137 -187
- data/docs/guides/using-tools.md +259 -212
- data/docs/index.md +46 -41
- data/examples/01_simple_robot.rb +6 -9
- data/examples/02_tools.rb +6 -9
- data/examples/03_network.rb +19 -17
- 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 +169 -0
- data/examples/09_chaining.rb +262 -0
- data/examples/10_memory.rb +331 -0
- data/examples/11_network_introspection.rb +253 -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 +63 -0
- data/examples/14_rusty_circuit/open_mic.rb +123 -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 +156 -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/16_writers_room/display.rb +158 -0
- data/examples/16_writers_room/output/.gitignore +2 -0
- data/examples/16_writers_room/output/opus_001.md +263 -0
- data/examples/16_writers_room/output/opus_001_notes.log +470 -0
- data/examples/16_writers_room/prompts/writer.md +37 -0
- data/examples/16_writers_room/room.rb +150 -0
- data/examples/16_writers_room/tools.rb +162 -0
- data/examples/16_writers_room/writer.rb +121 -0
- data/examples/16_writers_room/writers_room.rb +162 -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 +0 -13
- 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 +10 -34
- data/lib/robot_lab/network.rb +13 -20
- data/lib/robot_lab/robot/bus_messaging.rb +239 -0
- data/lib/robot_lab/robot/mcp_management.rb +88 -0
- data/lib/robot_lab/robot/template_rendering.rb +130 -0
- data/lib/robot_lab/robot.rb +240 -330
- data/lib/robot_lab/robot_message.rb +44 -0
- data/lib/robot_lab/robot_result.rb +1 -0
- data/lib/robot_lab/run_config.rb +184 -0
- data/lib/robot_lab/state_proxy.rb +2 -12
- data/lib/robot_lab/streaming/context.rb +1 -1
- data/lib/robot_lab/task.rb +8 -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/utils.rb +39 -0
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +89 -57
- data/mkdocs.yml +0 -11
- metadata +121 -135
- data/docs/api/adapters/anthropic.md +0 -121
- data/docs/api/adapters/gemini.md +0 -133
- data/docs/api/adapters/index.md +0 -104
- data/docs/api/adapters/openai.md +0 -134
- data/docs/api/history/active-record-adapter.md +0 -195
- data/docs/api/history/config.md +0 -191
- data/docs/api/history/index.md +0 -132
- data/docs/api/history/thread-manager.md +0 -144
- data/docs/guides/history.md +0 -359
- 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/adapters/anthropic.rb +0 -163
- data/lib/robot_lab/adapters/base.rb +0 -85
- data/lib/robot_lab/adapters/gemini.rb +0 -193
- data/lib/robot_lab/adapters/openai.rb +0 -159
- data/lib/robot_lab/adapters/registry.rb +0 -81
- data/lib/robot_lab/configuration.rb +0 -143
- data/lib/robot_lab/errors.rb +0 -70
- data/lib/robot_lab/history/active_record_adapter.rb +0 -146
- data/lib/robot_lab/history/config.rb +0 -115
- data/lib/robot_lab/history/thread_manager.rb +0 -93
- data/lib/robot_lab/robotic_model.rb +0 -324
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Example 13: Spawning Robots — Dynamic Specialist Creation
|
|
5
|
+
#
|
|
6
|
+
# A dispatcher robot receives a question, decides what kind of
|
|
7
|
+
# specialist is needed, spawns one on the fly, and hands off
|
|
8
|
+
# the work. The spawned robot replies via the shared message bus.
|
|
9
|
+
#
|
|
10
|
+
# This demonstrates Werner's "objects that create new objects to
|
|
11
|
+
# deal with problems nobody anticipated."
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# bundle exec ruby examples/13_spawn.rb
|
|
15
|
+
|
|
16
|
+
ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
|
|
17
|
+
|
|
18
|
+
require_relative "../lib/robot_lab"
|
|
19
|
+
|
|
20
|
+
RubyLLM.configure { |c| c.logger = Logger.new(File::NULL) }
|
|
21
|
+
|
|
22
|
+
QUESTIONS = [
|
|
23
|
+
"Why did the Roman Empire fall?",
|
|
24
|
+
"Write a haiku about recursion.",
|
|
25
|
+
"What is the square root of 144?",
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
class Dispatcher < RobotLab::Robot
|
|
29
|
+
attr_reader :spawned
|
|
30
|
+
|
|
31
|
+
def initialize(bus: nil)
|
|
32
|
+
super(name: "dispatcher", template: :dispatcher, bus: bus)
|
|
33
|
+
@spawned = {}
|
|
34
|
+
@pending = {}
|
|
35
|
+
|
|
36
|
+
on_message do |message|
|
|
37
|
+
puts " Dispatcher <- :#{message.from} replied"
|
|
38
|
+
puts " | #{message.content.to_s.lines.first&.strip}"
|
|
39
|
+
@pending.delete(message.from)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def dispatch(question)
|
|
44
|
+
# Ask the LLM which specialist to spawn
|
|
45
|
+
plan = run(question).reply.strip
|
|
46
|
+
role, instruction = plan.split("\n", 2)
|
|
47
|
+
role = role.strip.downcase.gsub(/\s+/, "_")
|
|
48
|
+
instruction = instruction&.strip || "You are a helpful #{role}."
|
|
49
|
+
|
|
50
|
+
puts " Dispatcher -> spawn :#{role}"
|
|
51
|
+
puts " | #{instruction}"
|
|
52
|
+
|
|
53
|
+
# Spawn the specialist (reuse if already spawned)
|
|
54
|
+
specialist = @spawned[role] ||= spawn(
|
|
55
|
+
name: role,
|
|
56
|
+
system_prompt: instruction
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Record what we're waiting for
|
|
60
|
+
@pending[role] = question
|
|
61
|
+
|
|
62
|
+
# Ask the specialist to work on the question
|
|
63
|
+
specialist.send_message(to: :dispatcher, content:
|
|
64
|
+
specialist.run(question).reply.strip
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def done?
|
|
69
|
+
@pending.empty?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ── main ──────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
dispatcher = Dispatcher.new
|
|
76
|
+
|
|
77
|
+
puts "=" * 60
|
|
78
|
+
puts "Example 13: Spawning Specialist Robots"
|
|
79
|
+
puts "=" * 60
|
|
80
|
+
|
|
81
|
+
QUESTIONS.each_with_index do |question, i|
|
|
82
|
+
puts
|
|
83
|
+
puts "Question #{i + 1}: #{question}"
|
|
84
|
+
dispatcher.dispatch(question)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
puts
|
|
88
|
+
puts "-" * 60
|
|
89
|
+
puts "Specialists spawned: #{dispatcher.spawned.keys.join(', ')}"
|
|
90
|
+
puts "Total robots on bus: #{dispatcher.spawned.size + 1}"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ── Comic Tools ─────────────────────────────────────────────
|
|
4
|
+
#
|
|
5
|
+
# Each tool accesses the owning robot via the `robot` accessor
|
|
6
|
+
# inherited from RobotLab::Tool.
|
|
7
|
+
|
|
8
|
+
class ReinventStyle < RobotLab::Tool
|
|
9
|
+
description "Completely rewrite your comedy persona and approach. " \
|
|
10
|
+
"Use when your current style is clearly not working. " \
|
|
11
|
+
"Be bold — try something totally different. " \
|
|
12
|
+
"The new style takes effect on your next bit."
|
|
13
|
+
|
|
14
|
+
param :new_persona, type: "string",
|
|
15
|
+
desc: "Your new comedy persona, style, and approach. " \
|
|
16
|
+
"Be specific: what kind of humor, what voice, what attitude."
|
|
17
|
+
|
|
18
|
+
def execute(new_persona:)
|
|
19
|
+
robot.pending_reinvention = new_persona
|
|
20
|
+
robot.style_changes += 1
|
|
21
|
+
robot.display&.comic_tool("[reinvent_style] -> #{new_persona[0..70]}...")
|
|
22
|
+
"Style reinvention accepted: #{new_persona}. " \
|
|
23
|
+
"Commit to this new approach starting now."
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class AdjustEnergy < RobotLab::Tool
|
|
28
|
+
description "Adjust your creative energy level. " \
|
|
29
|
+
"Higher (0.8-1.0) = wilder, riskier, more unpredictable. " \
|
|
30
|
+
"Lower (0.2-0.4) = tighter, more controlled, precise."
|
|
31
|
+
|
|
32
|
+
param :level, type: "number",
|
|
33
|
+
desc: "Energy level from 0.1 (very controlled) to 1.0 (unhinged)"
|
|
34
|
+
param :reason, type: "string",
|
|
35
|
+
desc: "Why you're adjusting", required: false
|
|
36
|
+
|
|
37
|
+
def execute(level:, reason: "tactical adjustment")
|
|
38
|
+
clamped = [[level.to_f, 0.1].max, 1.0].min
|
|
39
|
+
robot.with_temperature(clamped)
|
|
40
|
+
robot.display&.comic_tool("[adjust_energy] -> %.1f (%s)" % [clamped, reason])
|
|
41
|
+
"Energy adjusted to #{clamped}. Reason: #{reason}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class GetCoaching < RobotLab::Tool
|
|
46
|
+
description "Summon a comedy coach backstage for quick advice. " \
|
|
47
|
+
"Use when you're struggling with the crowd and need " \
|
|
48
|
+
"an outside perspective on what to try next."
|
|
49
|
+
|
|
50
|
+
param :situation, type: "string",
|
|
51
|
+
desc: "Describe what's happening and what you need help with"
|
|
52
|
+
|
|
53
|
+
def execute(situation:)
|
|
54
|
+
@coaches ||= {}
|
|
55
|
+
coach = @coaches["comedy_coach"] ||= begin
|
|
56
|
+
robot.coaches_spawned += 1
|
|
57
|
+
robot.spawn(
|
|
58
|
+
name: "comedy_coach",
|
|
59
|
+
system_prompt:
|
|
60
|
+
"You are a veteran comedy coach backstage at a live show. " \
|
|
61
|
+
"A comedian is struggling and needs quick, actionable advice. " \
|
|
62
|
+
"Be direct and specific. One paragraph max. Tell them " \
|
|
63
|
+
"exactly what to do differently in their next bit."
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
advice = coach.run(situation).reply.strip
|
|
67
|
+
robot.display&.comic_tool("[get_coaching] -> #{advice[0..70]}...")
|
|
68
|
+
advice
|
|
69
|
+
rescue => e
|
|
70
|
+
robot.display&.comic_tool("[get_coaching] ERROR: #{e.message}")
|
|
71
|
+
"Coach unavailable right now. Trust your instincts."
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ── The Comic ────────────────────────────────────────────────
|
|
77
|
+
#
|
|
78
|
+
# Has three tools, all with side effects on the robot itself:
|
|
79
|
+
#
|
|
80
|
+
# reinvent_style — queues a system prompt rewrite (applied next round)
|
|
81
|
+
# adjust_energy — changes the comic's temperature immediately
|
|
82
|
+
# get_coaching — spawns a comedy coach on the shared bus
|
|
83
|
+
#
|
|
84
|
+
# The LLM decides when to call them. The developer provides the
|
|
85
|
+
# mechanism; the robot provides the judgment.
|
|
86
|
+
#
|
|
87
|
+
# Listens on personal :comic channel for heckler feedback.
|
|
88
|
+
# Publishes performances to the shared :room channel.
|
|
89
|
+
#
|
|
90
|
+
class Comic < RobotLab::Robot
|
|
91
|
+
attr_accessor :round, :style_changes, :coaches_spawned,
|
|
92
|
+
:pending_reinvention, :display
|
|
93
|
+
|
|
94
|
+
def initialize(bus:, display:)
|
|
95
|
+
@round = 0
|
|
96
|
+
@style_changes = 0
|
|
97
|
+
@coaches_spawned = 0
|
|
98
|
+
@pending_reinvention = nil
|
|
99
|
+
@display = display
|
|
100
|
+
|
|
101
|
+
super(
|
|
102
|
+
name: "comic",
|
|
103
|
+
template: :open_mic_comic,
|
|
104
|
+
bus: bus,
|
|
105
|
+
local_tools: [
|
|
106
|
+
ReinventStyle.new(robot: self),
|
|
107
|
+
AdjustEnergy.new(robot: self),
|
|
108
|
+
GetCoaching.new(robot: self)
|
|
109
|
+
]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Listen on personal channel for heckler feedback
|
|
113
|
+
on_message do |message|
|
|
114
|
+
next unless message.from == "heckler"
|
|
115
|
+
|
|
116
|
+
@round += 1
|
|
117
|
+
|
|
118
|
+
# Build the prompt, injecting any queued style reinvention.
|
|
119
|
+
# This embeds self-modification in the user prompt rather than
|
|
120
|
+
# rewriting system messages, avoiding chat message ordering issues.
|
|
121
|
+
prompt = "Round #{@round}."
|
|
122
|
+
|
|
123
|
+
if @pending_reinvention
|
|
124
|
+
prompt += "\n\nSTYLE REINVENTION: You are now #{@pending_reinvention}. " \
|
|
125
|
+
"Commit fully to this new style. Abandon your previous approach."
|
|
126
|
+
@pending_reinvention = nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
prompt += "\n\nThe heckler just shouted: \"#{message.content}\"\n\n" \
|
|
130
|
+
"Process their feedback. If your material isn't landing, " \
|
|
131
|
+
"use your tools to adapt — reinvent your style, adjust " \
|
|
132
|
+
"your energy, or get coaching. Then deliver your next bit."
|
|
133
|
+
|
|
134
|
+
result = run(prompt)
|
|
135
|
+
bit = result.reply.strip
|
|
136
|
+
|
|
137
|
+
@display.comic("Comic [Round #{@round}]", bit)
|
|
138
|
+
|
|
139
|
+
# Publish to the room — heckler and scout both pick it up
|
|
140
|
+
send_message(to: :room, content: "ROUND #{@round}: #{bit}")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -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,63 @@
|
|
|
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 routed through the core processing guard,
|
|
15
|
+
# which serializes run() calls to prevent Async fiber interleaving.
|
|
16
|
+
# Sends feedback directly to the comic's personal channel.
|
|
17
|
+
#
|
|
18
|
+
class Heckler < RobotLab::Robot
|
|
19
|
+
attr_reader :rounds, :won_over
|
|
20
|
+
|
|
21
|
+
def initialize(bus:, display:)
|
|
22
|
+
@rounds = 0
|
|
23
|
+
@won_over = false
|
|
24
|
+
@display = display
|
|
25
|
+
|
|
26
|
+
super(name: "heckler", template: :open_mic_heckler, bus: bus)
|
|
27
|
+
|
|
28
|
+
# Handle incoming messages — the core processing guard
|
|
29
|
+
# serializes all deliveries, preventing concurrent run()
|
|
30
|
+
# calls from corrupting chat history.
|
|
31
|
+
on_message do |message|
|
|
32
|
+
next unless message.from == "comic"
|
|
33
|
+
next if @rounds >= MAX_ROUNDS
|
|
34
|
+
|
|
35
|
+
@rounds += 1
|
|
36
|
+
|
|
37
|
+
verdict = run(
|
|
38
|
+
"The comedian just said: \"#{message.content}\"\n\n" \
|
|
39
|
+
"React however feels right — heckle, counter-joke, " \
|
|
40
|
+
"show respect, or stay silent."
|
|
41
|
+
).reply.strip
|
|
42
|
+
|
|
43
|
+
# The heckler chose silence — no output, no feedback
|
|
44
|
+
next if verdict.match?(/\[SILENCE\]/i)
|
|
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
|
+
send_reply(to: message.from.to_sym, content: verdict, in_reply_to: message.key) if @rounds < MAX_ROUNDS
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Listen to the room for the comic's performances.
|
|
58
|
+
# Route through the core processing guard.
|
|
59
|
+
@bus.subscribe(:room) do |delivery|
|
|
60
|
+
handle_incoming_delivery(delivery)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
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 routed through the core processing guard
|
|
27
|
+
# (BusMessaging#handle_incoming_delivery), which serializes all
|
|
28
|
+
# run() calls to prevent Async fiber interleaving from corrupting
|
|
29
|
+
# chat history.
|
|
30
|
+
#
|
|
31
|
+
# Style reinventions are injected into the next round's user prompt
|
|
32
|
+
# rather than modifying the chat's system messages, avoiding message
|
|
33
|
+
# ordering issues while achieving genuine self-modification.
|
|
34
|
+
#
|
|
35
|
+
# Usage:
|
|
36
|
+
# bundle exec ruby examples/14_rusty_circuit/open_mic.rb
|
|
37
|
+
# bundle exec ruby examples/14_rusty_circuit/open_mic.rb --log show.log
|
|
38
|
+
|
|
39
|
+
ENV["ROBOT_LAB_TEMPLATE_PATH"] ||= File.join(__dir__, "prompts")
|
|
40
|
+
|
|
41
|
+
require_relative "../../lib/robot_lab"
|
|
42
|
+
require_relative "display"
|
|
43
|
+
|
|
44
|
+
RubyLLM.configure { |c| c.logger = Logger.new(File::NULL) }
|
|
45
|
+
|
|
46
|
+
MAX_ROUNDS = 5
|
|
47
|
+
|
|
48
|
+
require_relative "comic"
|
|
49
|
+
require_relative "heckler"
|
|
50
|
+
require_relative "scout"
|
|
51
|
+
|
|
52
|
+
# ── The Show ─────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
log_path = nil
|
|
55
|
+
if (idx = ARGV.index("--log"))
|
|
56
|
+
log_path = ARGV[idx + 1]
|
|
57
|
+
abort "Usage: #{$0} [--log FILE]" unless log_path
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
bus = TypedBus::MessageBus.new
|
|
61
|
+
display = Display.new(
|
|
62
|
+
scout_path: File.join(__dir__, "scout_notes.md"),
|
|
63
|
+
log_path: log_path
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Shared room channel — the comic publishes performances here,
|
|
67
|
+
# and both the heckler and scout subscribe independently.
|
|
68
|
+
bus.add_channel(:room, type: RobotLab::RobotMessage)
|
|
69
|
+
|
|
70
|
+
comic = Comic.new(bus: bus, display: display)
|
|
71
|
+
heckler = Heckler.new(bus: bus, display: display)
|
|
72
|
+
scout = Scout.new(bus: bus, display: display)
|
|
73
|
+
|
|
74
|
+
display.banner(<<~BANNER)
|
|
75
|
+
============================================================
|
|
76
|
+
THE RUSTY CIRCUIT — Tuesday Open Mic
|
|
77
|
+
Architecture Dominates Material
|
|
78
|
+
============================================================
|
|
79
|
+
|
|
80
|
+
Cast:
|
|
81
|
+
Comic — observational humor, armed with self-modification tools
|
|
82
|
+
Heckler — three-year regular, high standards, zero patience
|
|
83
|
+
Scout — talent network, sitting in the back with a notebook
|
|
84
|
+
BANNER
|
|
85
|
+
|
|
86
|
+
display.separator
|
|
87
|
+
|
|
88
|
+
# The opening bit — comic performs, then enters the feedback loop
|
|
89
|
+
opening = comic.run(
|
|
90
|
+
"You just stepped on stage at The Rusty Circuit open mic. " \
|
|
91
|
+
"The crowd looks tough. Do your opening bit."
|
|
92
|
+
).reply.strip
|
|
93
|
+
|
|
94
|
+
display.comic("Comic [Opening]", opening)
|
|
95
|
+
|
|
96
|
+
# Publish to room — both heckler and scout pick it up.
|
|
97
|
+
# The heckler reacts and sends feedback to the comic's personal
|
|
98
|
+
# channel, triggering the feedback loop:
|
|
99
|
+
# room → heckler → comic → room → heckler → comic → ...
|
|
100
|
+
# The loop terminates when the heckler stops replying (MAX_ROUNDS).
|
|
101
|
+
# The scout observes each round via :room, serialized by the core guard.
|
|
102
|
+
comic.send_message(to: :room, content: "OPENING: #{opening}")
|
|
103
|
+
|
|
104
|
+
display.separator
|
|
105
|
+
|
|
106
|
+
# Final verdict from the talent scout
|
|
107
|
+
verdict = scout.run(scout.verdict_prompt).reply.strip
|
|
108
|
+
|
|
109
|
+
display.verdict("Scout [FINAL VERDICT]", verdict)
|
|
110
|
+
|
|
111
|
+
display.stats(<<~STATS)
|
|
112
|
+
────────────────────────────────────────────────────────────
|
|
113
|
+
Show Stats:
|
|
114
|
+
Rounds performed: #{comic.round + 1} (opening + #{comic.round} rounds)
|
|
115
|
+
Style reinventions: #{comic.style_changes}
|
|
116
|
+
Coaches spawned: #{comic.coaches_spawned}
|
|
117
|
+
Analysts recruited: #{scout.analysts_spawned}
|
|
118
|
+
Heckler won over: #{heckler.won_over}
|
|
119
|
+
Total robots on bus: #{[comic, heckler, scout].size + comic.coaches_spawned + scout.analysts_spawned}
|
|
120
|
+
|
|
121
|
+
STATS
|
|
122
|
+
|
|
123
|
+
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]"
|