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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +124 -64
  3. data/docs/api/core/index.md +41 -15
  4. data/docs/api/core/memory.md +247 -29
  5. data/docs/api/core/network.md +255 -33
  6. data/docs/api/core/result.md +120 -32
  7. data/docs/api/core/robot.md +551 -61
  8. data/docs/api/core/state.md +87 -197
  9. data/docs/api/core/tool.md +165 -20
  10. data/docs/api/errors.md +110 -17
  11. data/docs/api/hooks.md +469 -0
  12. data/docs/api/index.md +80 -7
  13. data/docs/api/mcp/client.md +129 -35
  14. data/docs/api/mcp/index.md +164 -23
  15. data/docs/api/mcp/server.md +27 -3
  16. data/docs/api/mcp/transports.md +94 -22
  17. data/docs/api/messages/index.md +26 -3
  18. data/docs/api/messages/text-message.md +33 -11
  19. data/docs/api/messages/tool-call-message.md +27 -4
  20. data/docs/api/messages/tool-result-message.md +23 -4
  21. data/docs/api/messages/user-message.md +45 -8
  22. data/docs/api/skills.md +519 -0
  23. data/docs/api/streaming/context.md +28 -5
  24. data/docs/api/streaming/index.md +57 -11
  25. data/docs/api/support.md +846 -0
  26. data/docs/architecture/core-concepts.md +79 -31
  27. data/docs/architecture/index.md +86 -11
  28. data/docs/architecture/message-flow.md +66 -29
  29. data/docs/architecture/network-orchestration.md +145 -38
  30. data/docs/architecture/robot-execution.md +172 -90
  31. data/docs/architecture/state-management.md +31 -12
  32. data/docs/concepts.md +176 -21
  33. data/docs/examples/basic-chat.md +72 -19
  34. data/docs/examples/index.md +117 -31
  35. data/docs/examples/mcp-server.md +154 -45
  36. data/docs/examples/multi-robot-network.md +91 -21
  37. data/docs/examples/tool-usage.md +104 -37
  38. data/docs/getting-started/configuration.md +284 -97
  39. data/docs/getting-started/installation.md +53 -41
  40. data/docs/getting-started/quick-start.md +51 -6
  41. data/docs/guides/building-robots.md +258 -50
  42. data/docs/guides/creating-networks.md +214 -30
  43. data/docs/guides/hooks.md +141 -54
  44. data/docs/guides/knowledge.md +35 -4
  45. data/docs/guides/mcp-integration.md +211 -44
  46. data/docs/guides/memory.md +103 -12
  47. data/docs/guides/observability.md +95 -47
  48. data/docs/guides/streaming.md +184 -125
  49. data/docs/guides/using-tools.md +237 -17
  50. data/docs/index.md +36 -4
  51. data/examples/01_simple_robot.rb +2 -2
  52. data/examples/02_tools.rb +14 -4
  53. data/examples/03_network.rb +12 -7
  54. data/examples/04_mcp.rb +11 -4
  55. data/examples/05_streaming.rb +8 -5
  56. data/examples/06_prompt_templates.rb +13 -9
  57. data/examples/07_network_memory.rb +5 -5
  58. data/examples/08_llm_config.rb +20 -15
  59. data/examples/09_chaining.rb +4 -4
  60. data/examples/11_network_introspection.rb +4 -4
  61. data/examples/12_message_bus.rb +2 -2
  62. data/examples/13_spawn.rb +2 -2
  63. data/examples/14_rusty_circuit/README.md +1 -0
  64. data/examples/14_rusty_circuit/comic.rb +7 -3
  65. data/examples/14_rusty_circuit/display.rb +14 -0
  66. data/examples/14_rusty_circuit/heckler.rb +8 -6
  67. data/examples/14_rusty_circuit/open_mic.rb +17 -6
  68. data/examples/14_rusty_circuit/scout.rb +17 -10
  69. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
  70. data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
  71. data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
  72. data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
  73. data/examples/16_writers_room/writer.rb +22 -22
  74. data/examples/16_writers_room/writers_room.rb +2 -0
  75. data/examples/17_skills.rb +14 -13
  76. data/examples/18_rails/README.md +20 -1
  77. data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
  78. data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
  79. data/examples/18_rails/app/robots/chat_robot.rb +11 -0
  80. data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
  81. data/examples/19_token_tracking.rb +25 -9
  82. data/examples/20_circuit_breaker.rb +10 -7
  83. data/examples/21_learning_loop.rb +42 -16
  84. data/examples/22_context_compression.rb +23 -23
  85. data/examples/23_convergence.rb +24 -17
  86. data/examples/24_structured_delegation.rb +13 -8
  87. data/examples/25_history_search.rb +12 -8
  88. data/examples/27_incident_response/incident_response.rb +31 -13
  89. data/examples/28_mcp_discovery.rb +17 -13
  90. data/examples/29_ractor_tools.rb +4 -2
  91. data/examples/30_ractor_network.rb +22 -17
  92. data/examples/31_launch_assessment.rb +20 -9
  93. data/examples/32_newsletter_reader.rb +7 -2
  94. data/examples/33_stock_predictor.rb +34 -13
  95. data/examples/34_agentskills.rb +7 -3
  96. data/examples/35_hooks.rb +18 -8
  97. data/examples/README.md +199 -45
  98. data/examples/common.rb +79 -11
  99. data/examples/xyzzy.rb +8 -1
  100. data/lib/robot_lab/config.rb +10 -5
  101. data/lib/robot_lab/names.rb +402 -0
  102. data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
  103. data/lib/robot_lab/robot/bus_messaging.rb +16 -8
  104. data/lib/robot_lab/robot/template_rendering.rb +16 -3
  105. data/lib/robot_lab/robot.rb +23 -2
  106. data/lib/robot_lab/version.rb +1 -1
  107. data/lib/robot_lab.rb +21 -15
  108. data/mkdocs.yml +6 -1
  109. 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
- # ANTHROPIC_API_KEY=your_key ruby examples/06_prompt_templates.rb
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
- model: LLM[:default].model
199
+ **llm_opts
200
200
  )
201
201
 
202
202
  # Order Support Robot
203
203
  order_robot = RobotLab.build(
204
- model: LLM[:default].model,
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
- model: LLM[:default].model,
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
- model: LLM[:default].model,
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
- # ANTHROPIC_API_KEY=your_key ruby examples/07_network_memory.rb
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
- model: LLM[:default].model
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
- model: LLM[:default].model
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
- model: LLM[:default].model
164
+ **llm_opts
165
165
  )
166
166
 
167
167
  synthesizer = SynthesizerRobot.new(
168
168
  name: "synthesizer",
169
169
  template: :synthesizer,
170
- model: LLM[:default].model
170
+ **llm_opts
171
171
  )
172
172
 
173
173
  # -----------------------------------------------------------------------------
@@ -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
- # ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
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
- # ANTHROPIC_API_KEY=your_key ruby examples/08_llm_config.rb
29
- # ROBOT_LAB_ENV=test ANTHROPIC_API_KEY=your_key ruby examples/08_llm_config.rb
30
- # ROBOT_LAB_ENV=production ANTHROPIC_API_KEY=your_key ruby examples/08_llm_config.rb
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 API key status without revealing values
66
- api_key = config.ruby_llm.anthropic_api_key
67
- puts " anthropic_api_key: #{api_key ? '[SET via config]' : (ENV['ANTHROPIC_API_KEY'] ? '[SET via env]' : '(not set)')}"
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: \"gpt-5.4\", temperature: 0.5)"
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
- model: LLM[:default].model,
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: LLM[:default].model,
113
- provider: LLM[:default].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: "gpt-5.4", temperature: 0.5)
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=gpt-4
154
+ ROBOT_LAB_RUBY_LLM__MODEL=#{LLM[:default].model}
150
155
  ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=180
151
- ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
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
@@ -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
- model: LLM[:default].model,
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
- model: LLM[:default].model,
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
- model: LLM[:default].model,
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
- model: LLM[:default].model,
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(model: LLM[:default].model, name: "classifier", system_prompt: "Classify input")
60
- analyst = RobotLab.build(model: LLM[:default].model, name: "analyst", system_prompt: "Analyze data")
61
- writer = RobotLab.build(model: LLM[:default].model, name: "writer", system_prompt: "Write summary")
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(model: LLM[:default].model, name: "reviewer", system_prompt: "Review output")
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):"
@@ -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", model: LLM[:default].model, template: :comedian, bus: bus, temperature: TEMP_START)
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", model: LLM[:default].model, template: :comedy_critic, bus: bus)
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", model: LLM[:default].model, template: :dispatcher, bus: bus)
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
- model: LLM[:default].model,
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
- model: LLM[:default].model,
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
- model: LLM[:default].model,
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
- result = run(prompt)
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 routed through the core processing guard,
15
- # which serializes run() calls to prevent Async fiber interleaving.
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", model: LLM[:default].model, template: :open_mic_heckler, bus: bus)
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
- # Route through the core processing guard.
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
- handle_incoming_delivery(delivery)
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 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.
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
- verdict = scout.run(scout.verdict_prompt).reply.strip
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
- model: LLM[:default].model,
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 routed through the core processing guard
74
- # (BusMessaging#handle_incoming_delivery), which serializes all
75
- # run() calls to prevent Async fiber interleaving from corrupting
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
- model: LLM[:default].model,
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
- # Route through the core processing guard.
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
- handle_incoming_delivery(delivery)
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 = run(prompt).reply.strip
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
- model: LLM[:default].model,
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
- model: LLM[:default].model,
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
- model: LLM[:default].model,
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
- model: LLM[:default].model,
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.instance_variable_set(:@article, revised)
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
- model: LLM[:default].model,
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], model: LLM[:default].model, system_prompt: distro[:prompt])
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
- attr_reader :article
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
- robot_result = run(message, network_memory: @shared_memory)
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