robot_lab 0.2.6 → 0.2.7
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/README.md +124 -64
- data/docs/api/core/index.md +41 -15
- data/docs/api/core/memory.md +247 -29
- data/docs/api/core/network.md +255 -33
- data/docs/api/core/result.md +120 -32
- data/docs/api/core/robot.md +551 -61
- data/docs/api/core/state.md +87 -197
- data/docs/api/core/tool.md +165 -20
- data/docs/api/errors.md +110 -17
- data/docs/api/hooks.md +469 -0
- data/docs/api/index.md +80 -7
- data/docs/api/mcp/client.md +129 -35
- data/docs/api/mcp/index.md +164 -23
- data/docs/api/mcp/server.md +27 -3
- data/docs/api/mcp/transports.md +94 -22
- data/docs/api/messages/index.md +26 -3
- data/docs/api/messages/text-message.md +33 -11
- data/docs/api/messages/tool-call-message.md +27 -4
- data/docs/api/messages/tool-result-message.md +23 -4
- data/docs/api/messages/user-message.md +45 -8
- data/docs/api/skills.md +519 -0
- data/docs/api/streaming/context.md +28 -5
- data/docs/api/streaming/index.md +57 -11
- data/docs/api/support.md +846 -0
- data/docs/architecture/core-concepts.md +79 -31
- data/docs/architecture/index.md +86 -11
- data/docs/architecture/message-flow.md +66 -29
- data/docs/architecture/network-orchestration.md +145 -38
- data/docs/architecture/robot-execution.md +172 -90
- data/docs/architecture/state-management.md +31 -12
- data/docs/concepts.md +176 -21
- data/docs/examples/basic-chat.md +72 -19
- data/docs/examples/index.md +117 -31
- data/docs/examples/mcp-server.md +154 -45
- data/docs/examples/multi-robot-network.md +91 -21
- data/docs/examples/tool-usage.md +104 -37
- data/docs/getting-started/configuration.md +284 -97
- data/docs/getting-started/installation.md +53 -41
- data/docs/getting-started/quick-start.md +51 -6
- data/docs/guides/building-robots.md +258 -50
- data/docs/guides/creating-networks.md +214 -30
- data/docs/guides/hooks.md +141 -54
- data/docs/guides/knowledge.md +35 -4
- data/docs/guides/mcp-integration.md +211 -44
- data/docs/guides/memory.md +103 -12
- data/docs/guides/observability.md +95 -47
- data/docs/guides/streaming.md +184 -125
- data/docs/guides/using-tools.md +237 -17
- data/docs/index.md +36 -4
- data/examples/01_simple_robot.rb +2 -2
- data/examples/02_tools.rb +14 -4
- data/examples/03_network.rb +12 -7
- data/examples/04_mcp.rb +11 -4
- data/examples/05_streaming.rb +8 -5
- data/examples/06_prompt_templates.rb +13 -9
- data/examples/07_network_memory.rb +5 -5
- data/examples/08_llm_config.rb +20 -15
- data/examples/09_chaining.rb +4 -4
- data/examples/11_network_introspection.rb +4 -4
- data/examples/12_message_bus.rb +2 -2
- data/examples/13_spawn.rb +2 -2
- data/examples/14_rusty_circuit/README.md +1 -0
- data/examples/14_rusty_circuit/comic.rb +7 -3
- data/examples/14_rusty_circuit/display.rb +14 -0
- data/examples/14_rusty_circuit/heckler.rb +8 -6
- data/examples/14_rusty_circuit/open_mic.rb +17 -6
- data/examples/14_rusty_circuit/scout.rb +17 -10
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
- data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
- data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
- data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
- data/examples/16_writers_room/writer.rb +22 -22
- data/examples/16_writers_room/writers_room.rb +2 -0
- data/examples/17_skills.rb +14 -13
- data/examples/18_rails/README.md +20 -1
- data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
- data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
- data/examples/18_rails/app/robots/chat_robot.rb +11 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
- data/examples/19_token_tracking.rb +25 -9
- data/examples/20_circuit_breaker.rb +10 -7
- data/examples/21_learning_loop.rb +42 -16
- data/examples/22_context_compression.rb +23 -23
- data/examples/23_convergence.rb +24 -17
- data/examples/24_structured_delegation.rb +13 -8
- data/examples/25_history_search.rb +12 -8
- data/examples/27_incident_response/incident_response.rb +31 -13
- data/examples/28_mcp_discovery.rb +17 -13
- data/examples/29_ractor_tools.rb +4 -2
- data/examples/30_ractor_network.rb +22 -17
- data/examples/31_launch_assessment.rb +20 -9
- data/examples/32_newsletter_reader.rb +7 -2
- data/examples/33_stock_predictor.rb +34 -13
- data/examples/34_agentskills.rb +7 -3
- data/examples/35_hooks.rb +18 -8
- data/examples/README.md +199 -45
- data/examples/common.rb +79 -11
- data/examples/xyzzy.rb +8 -1
- data/lib/robot_lab/config.rb +10 -5
- data/lib/robot_lab/names.rb +402 -0
- data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
- data/lib/robot_lab/robot/bus_messaging.rb +16 -8
- data/lib/robot_lab/robot/template_rendering.rb +16 -3
- data/lib/robot_lab/robot.rb +23 -2
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +21 -15
- data/mkdocs.yml +6 -1
- metadata +7 -2
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# system with dynamic context injection using SimpleFlow's optional task routing.
|
|
9
9
|
#
|
|
10
10
|
# Usage:
|
|
11
|
-
#
|
|
11
|
+
# ruby examples/06_prompt_templates.rb
|
|
12
12
|
#
|
|
13
13
|
# Template Structure (prompt_manager .md files with YAML front matter):
|
|
14
14
|
# examples/prompts/
|
|
@@ -196,12 +196,12 @@ triage_robot = TriageRobot.new(
|
|
|
196
196
|
company_name: COMPANY_NAME,
|
|
197
197
|
categories: CATEGORIES
|
|
198
198
|
},
|
|
199
|
-
|
|
199
|
+
**llm_opts
|
|
200
200
|
)
|
|
201
201
|
|
|
202
202
|
# Order Support Robot
|
|
203
203
|
order_robot = RobotLab.build(
|
|
204
|
-
|
|
204
|
+
**llm_opts,
|
|
205
205
|
name: "order",
|
|
206
206
|
description: "Handles order-related inquiries with full order history",
|
|
207
207
|
template: :order_support,
|
|
@@ -215,7 +215,7 @@ order_robot = RobotLab.build(
|
|
|
215
215
|
|
|
216
216
|
# Product Support Robot
|
|
217
217
|
product_robot = RobotLab.build(
|
|
218
|
-
|
|
218
|
+
**llm_opts,
|
|
219
219
|
name: "product",
|
|
220
220
|
description: "Answers product questions with catalog knowledge",
|
|
221
221
|
template: :product_support,
|
|
@@ -230,7 +230,7 @@ product_robot = RobotLab.build(
|
|
|
230
230
|
|
|
231
231
|
# Escalation Robot
|
|
232
232
|
escalation_robot = RobotLab.build(
|
|
233
|
-
|
|
233
|
+
**llm_opts,
|
|
234
234
|
name: "escalation",
|
|
235
235
|
description: "Handles complex cases requiring special authority",
|
|
236
236
|
template: :escalation,
|
|
@@ -245,11 +245,15 @@ escalation_robot = RobotLab.build(
|
|
|
245
245
|
# Create Network with Optional Task Routing
|
|
246
246
|
# -----------------------------------------------------------------------------
|
|
247
247
|
|
|
248
|
+
# tools: :inherit on each task. Task#to_run_params forwards it to robot.run,
|
|
249
|
+
# which otherwise defaults to :none and would send the provider an empty tool
|
|
250
|
+
# list — the AskUser tool would be attached to each robot but never offered
|
|
251
|
+
# to the model.
|
|
248
252
|
network = RobotLab.create_network(name: "ecommerce_support") do
|
|
249
|
-
task :triage, triage_robot, depends_on: :none
|
|
250
|
-
task :order, order_robot, depends_on: :optional
|
|
251
|
-
task :product, product_robot, depends_on: :optional
|
|
252
|
-
task :escalation, escalation_robot, depends_on: :optional
|
|
253
|
+
task :triage, triage_robot, depends_on: :none, tools: :inherit
|
|
254
|
+
task :order, order_robot, depends_on: :optional, tools: :inherit
|
|
255
|
+
task :product, product_robot, depends_on: :optional, tools: :inherit
|
|
256
|
+
task :escalation, escalation_robot, depends_on: :optional, tools: :inherit
|
|
253
257
|
end
|
|
254
258
|
|
|
255
259
|
# -----------------------------------------------------------------------------
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
# └─────────────────────────────────────────────────────────────┘
|
|
40
40
|
#
|
|
41
41
|
# Usage:
|
|
42
|
-
#
|
|
42
|
+
# ruby examples/07_network_memory.rb
|
|
43
43
|
|
|
44
44
|
require_relative "common"
|
|
45
45
|
require "json"
|
|
@@ -147,27 +147,27 @@ sentiment_robot = AnalysisRobot.new(
|
|
|
147
147
|
name: "sentiment_analyzer",
|
|
148
148
|
template: :sentiment_analyzer,
|
|
149
149
|
memory_key: :sentiment,
|
|
150
|
-
|
|
150
|
+
**llm_opts
|
|
151
151
|
)
|
|
152
152
|
|
|
153
153
|
entity_robot = AnalysisRobot.new(
|
|
154
154
|
name: "entity_extractor",
|
|
155
155
|
template: :entity_extractor,
|
|
156
156
|
memory_key: :entities,
|
|
157
|
-
|
|
157
|
+
**llm_opts
|
|
158
158
|
)
|
|
159
159
|
|
|
160
160
|
keyword_robot = AnalysisRobot.new(
|
|
161
161
|
name: "keyword_extractor",
|
|
162
162
|
template: :keyword_extractor,
|
|
163
163
|
memory_key: :keywords,
|
|
164
|
-
|
|
164
|
+
**llm_opts
|
|
165
165
|
)
|
|
166
166
|
|
|
167
167
|
synthesizer = SynthesizerRobot.new(
|
|
168
168
|
name: "synthesizer",
|
|
169
169
|
template: :synthesizer,
|
|
170
|
-
|
|
170
|
+
**llm_opts
|
|
171
171
|
)
|
|
172
172
|
|
|
173
173
|
# -----------------------------------------------------------------------------
|
data/examples/08_llm_config.rb
CHANGED
|
@@ -20,14 +20,14 @@
|
|
|
20
20
|
# Environment is determined by ROBOT_LAB_ENV, RAILS_ENV, or RACK_ENV.
|
|
21
21
|
#
|
|
22
22
|
# Environment variable examples:
|
|
23
|
-
#
|
|
24
|
-
# ROBOT_LAB_RUBY_LLM__MODEL=gpt-4
|
|
23
|
+
# ROBOT_LAB_RUBY_LLM__MODEL=qwen3.6:latest
|
|
25
24
|
# ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=180
|
|
25
|
+
# OLLAMA_API_BASE=http://localhost:11434/v1
|
|
26
26
|
#
|
|
27
27
|
# Usage:
|
|
28
|
-
#
|
|
29
|
-
# ROBOT_LAB_ENV=test
|
|
30
|
-
# ROBOT_LAB_ENV=production
|
|
28
|
+
# ruby examples/08_llm_config.rb
|
|
29
|
+
# ROBOT_LAB_ENV=test ruby examples/08_llm_config.rb
|
|
30
|
+
# ROBOT_LAB_ENV=production ruby examples/08_llm_config.rb
|
|
31
31
|
|
|
32
32
|
require_relative "common"
|
|
33
33
|
|
|
@@ -62,9 +62,9 @@ puts " request_timeout: #{config.ruby_llm.request_timeout}s"
|
|
|
62
62
|
puts " max_retries: #{config.ruby_llm.max_retries}"
|
|
63
63
|
puts " log_level: #{config.ruby_llm.log_level}"
|
|
64
64
|
|
|
65
|
-
# Show
|
|
66
|
-
|
|
67
|
-
puts "
|
|
65
|
+
# Show where the LLM traffic actually goes
|
|
66
|
+
puts " ollama_api_base: #{OLLAMA_API_BASE}"
|
|
67
|
+
puts " (local inference — no API key in play)"
|
|
68
68
|
puts
|
|
69
69
|
|
|
70
70
|
section "Configuration Hierarchy (highest priority first)"
|
|
@@ -89,9 +89,12 @@ puts " 1. Bundled defaults: lib/robot_lab/config/defaults.yml + env overr
|
|
|
89
89
|
section "RunConfig: Shared Operational Defaults"
|
|
90
90
|
|
|
91
91
|
shared = RobotLab::RunConfig.new(model: LLM[:default].model, temperature: 0.5)
|
|
92
|
-
puts " shared = RunConfig.new(model:
|
|
92
|
+
puts " shared = RunConfig.new(model: #{LLM[:default].model.inspect}, temperature: 0.5)"
|
|
93
93
|
puts " shared.to_h => #{shared.to_h.inspect}"
|
|
94
94
|
puts
|
|
95
|
+
puts " Note: RunConfig carries no `provider` field, so an Ollama model still"
|
|
96
|
+
puts " needs provider: passed to each robot alongside the shared config."
|
|
97
|
+
puts
|
|
95
98
|
|
|
96
99
|
creative = RobotLab::RunConfig.new(temperature: 0.9)
|
|
97
100
|
merged = shared.merge(creative)
|
|
@@ -103,14 +106,16 @@ puts
|
|
|
103
106
|
|
|
104
107
|
# Create a robot using the configuration
|
|
105
108
|
robot = RobotLab.build(
|
|
106
|
-
|
|
109
|
+
**llm_opts,
|
|
107
110
|
name: "config_demo",
|
|
108
111
|
template: :llm_config_demo,
|
|
109
112
|
local_tools: [RobotLab::AskUser],
|
|
113
|
+
# These are TEMPLATE parameters, not robot kwargs — the template renders
|
|
114
|
+
# them into its prompt text.
|
|
110
115
|
context: {
|
|
111
116
|
environment: env,
|
|
112
|
-
model:
|
|
113
|
-
provider:
|
|
117
|
+
model: LLM[:default].model,
|
|
118
|
+
provider: LLM[:default].provider
|
|
114
119
|
}
|
|
115
120
|
)
|
|
116
121
|
|
|
@@ -141,14 +146,14 @@ puts <<~FOOTER
|
|
|
141
146
|
Per-Robot: RunConfig, template front matter, constructor params, with_*
|
|
142
147
|
|
|
143
148
|
RunConfig lets you express shared defaults that flow through the hierarchy:
|
|
144
|
-
shared = RobotLab::RunConfig.new(model:
|
|
149
|
+
shared = RobotLab::RunConfig.new(model: #{LLM[:default].model.inspect}, temperature: 0.5)
|
|
145
150
|
network = RobotLab.create_network(name: "team", config: shared) { ... }
|
|
146
151
|
robot = RobotLab.build(name: "bot", config: shared, temperature: 0.9)
|
|
147
152
|
|
|
148
153
|
Example environment variable overrides:
|
|
149
|
-
ROBOT_LAB_RUBY_LLM__MODEL
|
|
154
|
+
ROBOT_LAB_RUBY_LLM__MODEL=#{LLM[:default].model}
|
|
150
155
|
ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=180
|
|
151
|
-
|
|
156
|
+
OLLAMA_API_BASE=http://localhost:11434/v1
|
|
152
157
|
|
|
153
158
|
Try running with different environments:
|
|
154
159
|
ROBOT_LAB_ENV=test ruby examples/08_llm_config.rb
|
data/examples/09_chaining.rb
CHANGED
|
@@ -63,7 +63,7 @@ section "Section 1: Template Front Matter Config"
|
|
|
63
63
|
# The 'configurable' template sets temperature: 0.3 and max_tokens: 200
|
|
64
64
|
# via YAML front matter. These are applied automatically.
|
|
65
65
|
robot = RobotLab.build(
|
|
66
|
-
|
|
66
|
+
**llm_opts,
|
|
67
67
|
name: "chameleon",
|
|
68
68
|
template: :configurable,
|
|
69
69
|
context: { task_type: "analysis" }
|
|
@@ -116,7 +116,7 @@ section "Section 4: Constructor Params Override Front Matter"
|
|
|
116
116
|
# The configurable template sets temperature: 0.3 in front matter.
|
|
117
117
|
# Passing temperature: 0.9 to the constructor overrides it.
|
|
118
118
|
robot2 = RobotLab.build(
|
|
119
|
-
|
|
119
|
+
**llm_opts,
|
|
120
120
|
name: "override_demo",
|
|
121
121
|
template: :configurable,
|
|
122
122
|
context: { task_type: "creative" },
|
|
@@ -142,7 +142,7 @@ puts
|
|
|
142
142
|
|
|
143
143
|
# Robot inherits from RunConfig; constructor kwargs still override
|
|
144
144
|
robot3 = RobotLab.build(
|
|
145
|
-
|
|
145
|
+
**llm_opts,
|
|
146
146
|
name: "runconfig_demo",
|
|
147
147
|
template: :configurable,
|
|
148
148
|
context: { task_type: "analysis" },
|
|
@@ -214,7 +214,7 @@ puts
|
|
|
214
214
|
# Build a robot with AskUser — the template's task_type default ("general")
|
|
215
215
|
# is offered to the user, who can accept or override it.
|
|
216
216
|
interactive = RobotLab.build(
|
|
217
|
-
|
|
217
|
+
**llm_opts,
|
|
218
218
|
name: "interactive_demo",
|
|
219
219
|
template: :configurable,
|
|
220
220
|
local_tools: [RobotLab::AskUser]
|
|
@@ -56,9 +56,9 @@ shared_config = RobotLab::RunConfig.new(model: LLM[:default].model, temperature:
|
|
|
56
56
|
creative_config = RobotLab::RunConfig.new(temperature: 0.9)
|
|
57
57
|
|
|
58
58
|
# Build robots (no LLM calls, just instances)
|
|
59
|
-
classifier = RobotLab.build(
|
|
60
|
-
analyst = RobotLab.build(
|
|
61
|
-
writer = RobotLab.build(
|
|
59
|
+
classifier = RobotLab.build(**llm_opts, name: "classifier", system_prompt: "Classify input")
|
|
60
|
+
analyst = RobotLab.build(**llm_opts, name: "analyst", system_prompt: "Analyze data")
|
|
61
|
+
writer = RobotLab.build(**llm_opts, name: "writer", system_prompt: "Write summary")
|
|
62
62
|
|
|
63
63
|
# Build network with RunConfig, dependencies, and per-task config
|
|
64
64
|
network = RobotLab.create_network(name: "demo_pipeline", config: shared_config) do
|
|
@@ -140,7 +140,7 @@ puts
|
|
|
140
140
|
|
|
141
141
|
section "Section 3: Dynamic Robot Addition"
|
|
142
142
|
|
|
143
|
-
reviewer = RobotLab.build(
|
|
143
|
+
reviewer = RobotLab.build(**llm_opts, name: "reviewer", system_prompt: "Review output")
|
|
144
144
|
network.add_robot(reviewer)
|
|
145
145
|
|
|
146
146
|
puts "After add_robot(reviewer):"
|
data/examples/12_message_bus.rb
CHANGED
|
@@ -19,7 +19,7 @@ class Comedian < RobotLab::Robot
|
|
|
19
19
|
TEMP_STEP = 0.2
|
|
20
20
|
|
|
21
21
|
def initialize(bus:)
|
|
22
|
-
super(name: "bob",
|
|
22
|
+
super(name: "bob", **llm_opts, template: :comedian, bus: bus, temperature: TEMP_START)
|
|
23
23
|
@attempts = 0
|
|
24
24
|
on_message do |message|
|
|
25
25
|
@attempts += 1
|
|
@@ -36,7 +36,7 @@ end
|
|
|
36
36
|
|
|
37
37
|
class ComedyCritic < RobotLab::Robot
|
|
38
38
|
def initialize(bus:)
|
|
39
|
-
super(name: "alice",
|
|
39
|
+
super(name: "alice", **llm_opts, template: :comedy_critic, bus: bus)
|
|
40
40
|
@accepted = false
|
|
41
41
|
@rounds = 0
|
|
42
42
|
on_message do |message|
|
data/examples/13_spawn.rb
CHANGED
|
@@ -25,7 +25,7 @@ class Dispatcher < RobotLab::Robot
|
|
|
25
25
|
attr_reader :spawned
|
|
26
26
|
|
|
27
27
|
def initialize(bus: nil)
|
|
28
|
-
super(name: "dispatcher",
|
|
28
|
+
super(name: "dispatcher", **llm_opts, template: :dispatcher, bus: bus)
|
|
29
29
|
@spawned = {}
|
|
30
30
|
@pending = {}
|
|
31
31
|
|
|
@@ -49,7 +49,7 @@ class Dispatcher < RobotLab::Robot
|
|
|
49
49
|
# Spawn the specialist (reuse if already spawned)
|
|
50
50
|
specialist = @spawned[role] ||= spawn(
|
|
51
51
|
name: role,
|
|
52
|
-
|
|
52
|
+
**llm_opts,
|
|
53
53
|
system_prompt: instruction
|
|
54
54
|
)
|
|
55
55
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
execute the open_mic.rb file
|
|
@@ -56,7 +56,7 @@ class GetCoaching < RobotLab::Tool
|
|
|
56
56
|
robot.coaches_spawned += 1
|
|
57
57
|
robot.spawn(
|
|
58
58
|
name: "comedy_coach",
|
|
59
|
-
|
|
59
|
+
**llm_opts,
|
|
60
60
|
system_prompt:
|
|
61
61
|
"You are a veteran comedy coach backstage at a live show. " \
|
|
62
62
|
"A comedian is struggling and needs quick, actionable advice. " \
|
|
@@ -64,6 +64,7 @@ class GetCoaching < RobotLab::Tool
|
|
|
64
64
|
"exactly what to do differently in their next bit."
|
|
65
65
|
)
|
|
66
66
|
end
|
|
67
|
+
robot.display&.working("Comic", "backstage with the coach")
|
|
67
68
|
advice = coach.run(situation).reply.strip
|
|
68
69
|
robot.display&.comic_tool("[get_coaching] -> #{advice[0..70]}...")
|
|
69
70
|
advice
|
|
@@ -101,7 +102,7 @@ class Comic < RobotLab::Robot
|
|
|
101
102
|
|
|
102
103
|
super(
|
|
103
104
|
name: "comic",
|
|
104
|
-
|
|
105
|
+
**llm_opts,
|
|
105
106
|
template: :open_mic_comic,
|
|
106
107
|
bus: bus,
|
|
107
108
|
local_tools: [
|
|
@@ -133,7 +134,10 @@ class Comic < RobotLab::Robot
|
|
|
133
134
|
"use your tools to adapt — reinvent your style, adjust " \
|
|
134
135
|
"your energy, or get coaching. Then deliver your next bit."
|
|
135
136
|
|
|
136
|
-
|
|
137
|
+
# tools: :inherit — without it run() defaults to :none and the comic's
|
|
138
|
+
# three self-modification tools are never offered to the model, which
|
|
139
|
+
# kills the whole point of the demo.
|
|
140
|
+
result = run(prompt, tools: :inherit)
|
|
137
141
|
bit = result.reply.strip
|
|
138
142
|
|
|
139
143
|
@display.comic("Comic [Round #{@round}]", bit)
|
|
@@ -69,6 +69,20 @@ class Display
|
|
|
69
69
|
log(" #{text}")
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
# ── Liveness ────────────────────────────────────────────
|
|
73
|
+
#
|
|
74
|
+
# The scout writes its notes to a file, so a scout turn produces NO
|
|
75
|
+
# terminal output at all — and a scout turn is 1-4 sequential LLM calls
|
|
76
|
+
# (observe, then any tool round-trips, then a spawned analyst's own run).
|
|
77
|
+
# On a local model that is minutes of dead-silent terminal, which reads
|
|
78
|
+
# as a hang. These lines prove the show is still moving.
|
|
79
|
+
|
|
80
|
+
def working(who, what)
|
|
81
|
+
puts Rainbow(" · #{who} #{what}…").darkgray
|
|
82
|
+
$stdout.flush
|
|
83
|
+
log(" · #{who} #{what}...")
|
|
84
|
+
end
|
|
85
|
+
|
|
72
86
|
# ── Scout (file only) ───────────────────────────────────
|
|
73
87
|
|
|
74
88
|
def scout(round_num, notes)
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
# own jokes using the comedian as the punch line.
|
|
12
12
|
#
|
|
13
13
|
# Subscribes to the :room channel to hear performances.
|
|
14
|
-
# Room deliveries are
|
|
15
|
-
# which serializes run() calls
|
|
14
|
+
# Room deliveries are handed to the robot's BusPoller via
|
|
15
|
+
# enqueue_delivery, which serializes run() calls so Async fiber
|
|
16
|
+
# interleaving can't corrupt chat history.
|
|
16
17
|
# Sends feedback directly to the comic's personal channel.
|
|
17
18
|
#
|
|
18
19
|
class Heckler < RobotLab::Robot
|
|
@@ -23,7 +24,7 @@ class Heckler < RobotLab::Robot
|
|
|
23
24
|
@won_over = false
|
|
24
25
|
@display = display
|
|
25
26
|
|
|
26
|
-
super(name: "heckler",
|
|
27
|
+
super(name: "heckler", **llm_opts, template: :open_mic_heckler, bus: bus)
|
|
27
28
|
|
|
28
29
|
# Handle incoming messages — the core processing guard
|
|
29
30
|
# serializes all deliveries, preventing concurrent run()
|
|
@@ -54,10 +55,11 @@ class Heckler < RobotLab::Robot
|
|
|
54
55
|
send_reply(to: message.from.to_sym, content: verdict, in_reply_to: message.key) if @rounds < MAX_ROUNDS
|
|
55
56
|
end
|
|
56
57
|
|
|
57
|
-
# Listen to the room for the comic's performances.
|
|
58
|
-
#
|
|
58
|
+
# Listen to the room for the comic's performances. Deliveries go to
|
|
59
|
+
# the robot's own BusPoller, which serializes them behind any run()
|
|
60
|
+
# already in flight before handing them to the on_message handler.
|
|
59
61
|
@bus.subscribe(:room) do |delivery|
|
|
60
|
-
|
|
62
|
+
enqueue_delivery(delivery)
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
end
|
|
@@ -23,15 +23,24 @@
|
|
|
23
23
|
# Communication uses a shared :room channel — the comic publishes
|
|
24
24
|
# performances there, and both the heckler and scout subscribe.
|
|
25
25
|
# The heckler sends feedback directly to the comic's personal channel.
|
|
26
|
-
# Room deliveries are
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
# chat history.
|
|
26
|
+
# Room deliveries are handed to each robot's BusPoller via
|
|
27
|
+
# BusMessaging#enqueue_delivery, which serializes all run() calls so
|
|
28
|
+
# Async fiber interleaving can't corrupt chat history.
|
|
30
29
|
#
|
|
31
30
|
# Style reinventions are injected into the next round's user prompt
|
|
32
31
|
# rather than modifying the chat's system messages, avoiding message
|
|
33
32
|
# ordering issues while achieving genuine self-modification.
|
|
34
33
|
#
|
|
34
|
+
# RUNTIME: this is the most expensive example in the suite. A full show is
|
|
35
|
+
# ~30 LLM calls (16 minimum, plus a round-trip per tool call and a full run
|
|
36
|
+
# for every robot the comic or scout spawns). On a 20B+ local model at ~60s
|
|
37
|
+
# per call that is 30+ minutes. Long silent stretches are the scout thinking,
|
|
38
|
+
# not a hang — it writes notes to output/scout_notes.md rather than STDOUT,
|
|
39
|
+
# so the "· Scout …" lines are your liveness signal.
|
|
40
|
+
#
|
|
41
|
+
# For a demo you can watch in a few minutes, point it at a smaller model:
|
|
42
|
+
# LLM_PROFILE=small bundle exec ruby examples/14_rusty_circuit/open_mic.rb
|
|
43
|
+
#
|
|
35
44
|
# Usage:
|
|
36
45
|
# bundle exec ruby examples/14_rusty_circuit/open_mic.rb
|
|
37
46
|
# bundle exec ruby examples/14_rusty_circuit/open_mic.rb --log show.log
|
|
@@ -86,7 +95,8 @@ display.separator
|
|
|
86
95
|
# The opening bit — comic performs, then enters the feedback loop
|
|
87
96
|
opening = comic.run(
|
|
88
97
|
"You just stepped on stage at The Rusty Circuit open mic. " \
|
|
89
|
-
"The crowd looks tough. Do your opening bit."
|
|
98
|
+
"The crowd looks tough. Do your opening bit.",
|
|
99
|
+
tools: :inherit
|
|
90
100
|
).reply.strip
|
|
91
101
|
|
|
92
102
|
display.comic("Comic [Opening]", opening)
|
|
@@ -102,7 +112,8 @@ comic.send_message(to: :room, content: "OPENING: #{opening}")
|
|
|
102
112
|
display.separator
|
|
103
113
|
|
|
104
114
|
# Final verdict from the talent scout
|
|
105
|
-
|
|
115
|
+
display.working("Scout", "writing the final verdict")
|
|
116
|
+
verdict = scout.run(scout.verdict_prompt, tools: :inherit).reply.strip
|
|
106
117
|
|
|
107
118
|
display.verdict("Scout [FINAL VERDICT]", verdict)
|
|
108
119
|
|
|
@@ -22,7 +22,7 @@ class RecruitAnalyst < RobotLab::Tool
|
|
|
22
22
|
robot.analysts_spawned += 1
|
|
23
23
|
robot.spawn(
|
|
24
24
|
name: "#{specialty}_analyst",
|
|
25
|
-
|
|
25
|
+
**llm_opts,
|
|
26
26
|
system_prompt:
|
|
27
27
|
"You are an expert #{specialty.tr('_', ' ')} analyst " \
|
|
28
28
|
"for stand-up comedy. You've studied the craft for decades. " \
|
|
@@ -30,6 +30,7 @@ class RecruitAnalyst < RobotLab::Tool
|
|
|
30
30
|
"and insightful. 2-3 sentences max."
|
|
31
31
|
)
|
|
32
32
|
end
|
|
33
|
+
robot.display&.working("Scout", "consulting the #{specialty.tr('_', ' ')} analyst")
|
|
33
34
|
analysis = analyst.run(robot.log.join("\n")).reply.strip
|
|
34
35
|
robot.display&.scout_analyst(specialty, analysis)
|
|
35
36
|
analysis
|
|
@@ -70,10 +71,9 @@ end
|
|
|
70
71
|
# analysts when they see something worth examining closely.
|
|
71
72
|
#
|
|
72
73
|
# Subscribes to the :room channel to observe performances.
|
|
73
|
-
# Room deliveries are
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
# chat history.
|
|
74
|
+
# Room deliveries are handed to the robot's BusPoller via
|
|
75
|
+
# BusMessaging#enqueue_delivery, which serializes all run() calls so
|
|
76
|
+
# Async fiber interleaving can't corrupt chat history.
|
|
77
77
|
#
|
|
78
78
|
class Scout < RobotLab::Robot
|
|
79
79
|
attr_accessor :log, :analysts_spawned, :pending_criteria, :display
|
|
@@ -86,7 +86,7 @@ class Scout < RobotLab::Robot
|
|
|
86
86
|
|
|
87
87
|
super(
|
|
88
88
|
name: "scout",
|
|
89
|
-
|
|
89
|
+
**llm_opts,
|
|
90
90
|
template: :open_mic_scout,
|
|
91
91
|
bus: bus,
|
|
92
92
|
local_tools: [
|
|
@@ -103,10 +103,11 @@ class Scout < RobotLab::Robot
|
|
|
103
103
|
observe_and_note(message.content.to_s)
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
# Listen to the room for the comic's performances.
|
|
107
|
-
#
|
|
106
|
+
# Listen to the room for the comic's performances. Deliveries go to
|
|
107
|
+
# the robot's own BusPoller, which serializes them behind any run()
|
|
108
|
+
# already in flight before handing them to the on_message handler.
|
|
108
109
|
@bus.subscribe(:room) do |delivery|
|
|
109
|
-
|
|
110
|
+
enqueue_delivery(delivery)
|
|
110
111
|
end
|
|
111
112
|
end
|
|
112
113
|
|
|
@@ -151,7 +152,13 @@ class Scout < RobotLab::Robot
|
|
|
151
152
|
"patterns, consider recruiting an analyst or refining " \
|
|
152
153
|
"your evaluation criteria."
|
|
153
154
|
|
|
154
|
-
notes
|
|
155
|
+
# The scout's notes go to a file, so announce the turn on STDOUT first —
|
|
156
|
+
# otherwise this is several minutes of silent terminal on a local model.
|
|
157
|
+
@display.working("Scout", "taking notes on round #{@log.size}")
|
|
158
|
+
|
|
159
|
+
# tools: :inherit — run() defaults to :none, which would leave
|
|
160
|
+
# recruit_analyst and refine_criteria attached but never offered.
|
|
161
|
+
notes = run(prompt, tools: :inherit).reply.strip
|
|
155
162
|
|
|
156
163
|
@display.scout(@log.size, notes)
|
|
157
164
|
end
|
|
@@ -57,7 +57,7 @@ bus = TypedBus::MessageBus.new
|
|
|
57
57
|
|
|
58
58
|
mac_writer = OsWriter.new(
|
|
59
59
|
name: "mac_writer",
|
|
60
|
-
|
|
60
|
+
**llm_opts,
|
|
61
61
|
template: :os_advocate,
|
|
62
62
|
local_tools: [RobotLab::AskUser],
|
|
63
63
|
context: {
|
|
@@ -69,7 +69,7 @@ mac_writer = OsWriter.new(
|
|
|
69
69
|
|
|
70
70
|
win_writer = OsWriter.new(
|
|
71
71
|
name: "win_writer",
|
|
72
|
-
|
|
72
|
+
**llm_opts,
|
|
73
73
|
template: :os_advocate,
|
|
74
74
|
local_tools: [RobotLab::AskUser],
|
|
75
75
|
context: {
|
|
@@ -81,7 +81,7 @@ win_writer = OsWriter.new(
|
|
|
81
81
|
|
|
82
82
|
linux_writer = LinuxWriter.new(
|
|
83
83
|
name: "linux_writer",
|
|
84
|
-
|
|
84
|
+
**llm_opts,
|
|
85
85
|
template: :os_advocate,
|
|
86
86
|
local_tools: [RobotLab::AskUser],
|
|
87
87
|
context: {
|
|
@@ -94,7 +94,7 @@ linux_writer = LinuxWriter.new(
|
|
|
94
94
|
|
|
95
95
|
editor = OsEditor.new(
|
|
96
96
|
name: "editor",
|
|
97
|
-
|
|
97
|
+
**llm_opts,
|
|
98
98
|
template: :os_editor,
|
|
99
99
|
bus: bus
|
|
100
100
|
)
|
|
@@ -103,7 +103,7 @@ editor = OsEditor.new(
|
|
|
103
103
|
revision_count = 0
|
|
104
104
|
editor.on_message do |message|
|
|
105
105
|
revised = editor.run(message.content).reply.strip
|
|
106
|
-
editor.
|
|
106
|
+
editor.article = revised
|
|
107
107
|
|
|
108
108
|
revision_count += 1
|
|
109
109
|
path = File.join(OUTPUT_DIR, "revision_#{revision_count}.md")
|
|
@@ -116,16 +116,20 @@ end
|
|
|
116
116
|
|
|
117
117
|
chief = EditorInChief.new(
|
|
118
118
|
name: "chief",
|
|
119
|
-
|
|
119
|
+
**llm_opts,
|
|
120
120
|
template: :os_chief,
|
|
121
121
|
bus: bus
|
|
122
122
|
)
|
|
123
123
|
|
|
124
|
-
# Network orchestrates the writing pipeline
|
|
124
|
+
# Network orchestrates the writing pipeline.
|
|
125
|
+
#
|
|
126
|
+
# tools: :inherit on the writer tasks — Task forwards it to robot.run, which
|
|
127
|
+
# defaults to :none and would otherwise hand the provider an empty tool list,
|
|
128
|
+
# leaving each writer's AskUser tool attached but never offered to the model.
|
|
125
129
|
network = RobotLab.create_network(name: "os_editorial") do
|
|
126
|
-
task :mac_writer, mac_writer, depends_on: :none
|
|
127
|
-
task :win_writer, win_writer, depends_on: :none
|
|
128
|
-
task :linux_writer, linux_writer, depends_on: :none
|
|
130
|
+
task :mac_writer, mac_writer, depends_on: :none, tools: :inherit
|
|
131
|
+
task :win_writer, win_writer, depends_on: :none, tools: :inherit
|
|
132
|
+
task :linux_writer, linux_writer, depends_on: :none, tools: :inherit
|
|
129
133
|
task :editor, editor, depends_on: [:mac_writer, :win_writer, :linux_writer]
|
|
130
134
|
end
|
|
131
135
|
|
|
@@ -26,7 +26,7 @@ class LinuxWriter < OsWriter
|
|
|
26
26
|
#{distro_analyses}
|
|
27
27
|
PROMPT
|
|
28
28
|
|
|
29
|
-
robot_result = run(enriched, network_memory: @shared_memory)
|
|
29
|
+
robot_result = run(enriched, network_memory: @shared_memory, tools: :inherit)
|
|
30
30
|
|
|
31
31
|
if @shared_memory
|
|
32
32
|
draft = robot_result.reply.to_s
|
|
@@ -57,7 +57,7 @@ class LinuxWriter < OsWriter
|
|
|
57
57
|
|
|
58
58
|
distros.each do |distro|
|
|
59
59
|
puts " [#{@name}] Spawning #{distro[:label]} specialist..."
|
|
60
|
-
specialist = spawn(name: distro[:name],
|
|
60
|
+
specialist = spawn(name: distro[:name], **llm_opts, system_prompt: distro[:prompt])
|
|
61
61
|
@specialists << specialist
|
|
62
62
|
|
|
63
63
|
analysis = specialist.run(
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
# extract_run_context mutation issue with parallel pipeline steps.
|
|
8
8
|
class OsEditor < RobotLab::Robot
|
|
9
9
|
attr_accessor :shared_memory
|
|
10
|
-
|
|
10
|
+
# article is writable so the chief's revision loop can install a revised
|
|
11
|
+
# draft without reaching for instance_variable_set.
|
|
12
|
+
attr_accessor :article
|
|
11
13
|
|
|
12
14
|
def initialize(**opts)
|
|
13
15
|
super(**opts)
|
|
@@ -18,7 +18,10 @@ class OsWriter < RobotLab::Robot
|
|
|
18
18
|
def call(result)
|
|
19
19
|
message = extract_message(result)
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
# tools: :inherit is passed explicitly. This override bypasses
|
|
22
|
+
# extract_run_context (see the note above), so the task's tools setting
|
|
23
|
+
# never reaches run() on its own — and run() defaults to :none.
|
|
24
|
+
robot_result = run(message, network_memory: @shared_memory, tools: :inherit)
|
|
22
25
|
|
|
23
26
|
if @shared_memory
|
|
24
27
|
draft = robot_result.reply.to_s
|