robot_lab 0.1.0 → 0.2.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.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/.architecture/AGENTS.md +32 -0
  3. data/.architecture/config.yml +8 -0
  4. data/.architecture/members.yml +60 -0
  5. data/.architecture/reviews/feature-free-will.md +490 -0
  6. data/.architecture/reviews/overall-codebase.md +427 -0
  7. data/.claude/settings.local.json +57 -0
  8. data/.codex/config.toml +2 -0
  9. data/.envrc +1 -0
  10. data/.irbrc +2 -2
  11. data/.loki +60 -0
  12. data/.quality/reek_baseline.txt +43 -0
  13. data/.rubocop.yml +10 -0
  14. data/CHANGELOG.md +126 -0
  15. data/CLAUDE.md +139 -0
  16. data/README.md +130 -73
  17. data/Rakefile +131 -9
  18. data/agent2agent_review.md +192 -0
  19. data/agentf_improvements.md +253 -0
  20. data/agents.md +14 -0
  21. data/docs/api/core/index.md +1 -0
  22. data/docs/api/core/network.md +31 -1
  23. data/docs/api/core/robot.md +91 -5
  24. data/docs/api/errors.md +47 -2
  25. data/docs/api/index.md +3 -0
  26. data/docs/architecture/core-concepts.md +40 -0
  27. data/docs/architecture/network-orchestration.md +8 -0
  28. data/docs/architecture/robot-execution.md +1 -1
  29. data/docs/examples/index.md +37 -2
  30. data/docs/getting-started/configuration.md +39 -7
  31. data/docs/guides/building-robots.md +39 -1
  32. data/docs/guides/creating-networks.md +6 -1
  33. data/docs/guides/hooks.md +997 -0
  34. data/docs/guides/index.md +19 -14
  35. data/docs/guides/knowledge.md +9 -3
  36. data/docs/guides/observability.md +206 -7
  37. data/docs/guides/using-tools.md +69 -0
  38. data/docs/index.md +31 -4
  39. data/docs/superpowers/plans/2026-05-06-agentskills.md +1303 -0
  40. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +247 -0
  41. data/examples/.envrc +1 -0
  42. data/examples/01_simple_robot.rb +5 -9
  43. data/examples/02_tools.rb +5 -9
  44. data/examples/03_network.rb +18 -10
  45. data/examples/04_mcp.rb +21 -29
  46. data/examples/05_streaming.rb +12 -18
  47. data/examples/06_prompt_templates.rb +11 -19
  48. data/examples/07_network_memory.rb +16 -31
  49. data/examples/08_llm_config.rb +10 -22
  50. data/examples/09_chaining.rb +16 -27
  51. data/examples/10_memory.rb +12 -28
  52. data/examples/11_network_introspection.rb +15 -29
  53. data/examples/12_message_bus.rb +5 -12
  54. data/examples/13_spawn.rb +5 -10
  55. data/examples/14_rusty_circuit/.envrc +1 -0
  56. data/examples/14_rusty_circuit/comic.rb +2 -0
  57. data/examples/14_rusty_circuit/heckler.rb +1 -1
  58. data/examples/14_rusty_circuit/open_mic.rb +1 -3
  59. data/examples/14_rusty_circuit/scout.rb +2 -0
  60. data/examples/15_memory_network_and_bus/.envrc +1 -0
  61. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +6 -3
  62. data/examples/15_memory_network_and_bus/linux_writer.rb +1 -1
  63. data/examples/15_memory_network_and_bus/output/combined_article.md +6 -6
  64. data/examples/15_memory_network_and_bus/output/final_article.md +6 -8
  65. data/examples/15_memory_network_and_bus/output/linux_draft.md +4 -2
  66. data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
  67. data/examples/15_memory_network_and_bus/output/memory.json +6 -6
  68. data/examples/15_memory_network_and_bus/output/revision_1.md +10 -11
  69. data/examples/15_memory_network_and_bus/output/revision_2.md +6 -8
  70. data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
  71. data/examples/16_writers_room/.envrc +1 -0
  72. data/examples/16_writers_room/writers_room.rb +2 -4
  73. data/examples/17_skills.rb +8 -17
  74. data/examples/18_rails/Gemfile +1 -0
  75. data/examples/19_token_tracking.rb +9 -15
  76. data/examples/20_circuit_breaker.rb +10 -19
  77. data/examples/21_learning_loop.rb +11 -20
  78. data/examples/22_context_compression.rb +6 -13
  79. data/examples/23_convergence.rb +6 -17
  80. data/examples/24_structured_delegation.rb +11 -15
  81. data/examples/25_history_search.rb +5 -12
  82. data/examples/26_document_store.rb +6 -13
  83. data/examples/27_incident_response/incident_response.rb +4 -5
  84. data/examples/28_mcp_discovery.rb +8 -11
  85. data/examples/29_ractor_tools.rb +4 -9
  86. data/examples/30_ractor_network.rb +10 -19
  87. data/examples/31_launch_assessment.rb +10 -23
  88. data/examples/32_newsletter_reader.rb +188 -0
  89. data/examples/33_stock_generator.rb +80 -0
  90. data/examples/33_stock_predictor.rb +306 -0
  91. data/examples/34_agentskills.rb +72 -0
  92. data/examples/35_hooks.rb +256 -0
  93. data/examples/README.md +8 -1
  94. data/examples/common.rb +80 -0
  95. data/examples/ruboruby.md +423 -0
  96. data/examples/xyzzy.rb +90 -0
  97. data/lib/robot_lab/agent_skill.rb +64 -0
  98. data/lib/robot_lab/agent_skill_catalog.rb +74 -0
  99. data/lib/robot_lab/ask_user.rb +2 -2
  100. data/lib/robot_lab/budget/ledger.rb +98 -0
  101. data/lib/robot_lab/bus_poller.rb +12 -5
  102. data/lib/robot_lab/capabilities.rb +84 -0
  103. data/lib/robot_lab/config/defaults.yml +10 -0
  104. data/lib/robot_lab/config.rb +5 -16
  105. data/lib/robot_lab/delegation_future.rb +1 -1
  106. data/lib/robot_lab/doom_loop_detector.rb +98 -0
  107. data/lib/robot_lab/error.rb +34 -4
  108. data/lib/robot_lab/errors.rb +45 -0
  109. data/lib/robot_lab/history_compressor.rb +4 -10
  110. data/lib/robot_lab/hook.rb +79 -0
  111. data/lib/robot_lab/hook_context.rb +194 -0
  112. data/lib/robot_lab/hook_registry.rb +55 -0
  113. data/lib/robot_lab/hooks.rb +87 -0
  114. data/lib/robot_lab/mcp/client.rb +1 -2
  115. data/lib/robot_lab/mcp/connection_poller.rb +5 -5
  116. data/lib/robot_lab/mcp/server.rb +1 -1
  117. data/lib/robot_lab/mcp/server_discovery.rb +0 -2
  118. data/lib/robot_lab/memory.rb +32 -27
  119. data/lib/robot_lab/memory_change.rb +2 -2
  120. data/lib/robot_lab/message.rb +4 -4
  121. data/lib/robot_lab/narrator.rb +87 -0
  122. data/lib/robot_lab/network.rb +87 -25
  123. data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
  124. data/lib/robot_lab/robot/budget.rb +89 -0
  125. data/lib/robot_lab/robot/bus_messaging.rb +79 -33
  126. data/lib/robot_lab/robot/history_search.rb +4 -1
  127. data/lib/robot_lab/robot/hooking.rb +56 -0
  128. data/lib/robot_lab/robot/mcp_management.rb +12 -12
  129. data/lib/robot_lab/robot/template_rendering.rb +62 -42
  130. data/lib/robot_lab/robot.rb +482 -242
  131. data/lib/robot_lab/robot_result.rb +6 -5
  132. data/lib/robot_lab/run_config.rb +27 -14
  133. data/lib/robot_lab/runnable.rb +51 -0
  134. data/lib/robot_lab/sandbox/null.rb +13 -0
  135. data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
  136. data/lib/robot_lab/sandbox.rb +52 -0
  137. data/lib/robot_lab/script_tool.rb +134 -0
  138. data/lib/robot_lab/state_proxy.rb +7 -5
  139. data/lib/robot_lab/task.rb +26 -20
  140. data/lib/robot_lab/tool.rb +54 -13
  141. data/lib/robot_lab/tool_config.rb +1 -1
  142. data/lib/robot_lab/tool_manifest.rb +5 -7
  143. data/lib/robot_lab/user_message.rb +2 -2
  144. data/lib/robot_lab/version.rb +1 -1
  145. data/lib/robot_lab/waiter.rb +1 -1
  146. data/lib/robot_lab.rb +89 -51
  147. data/logfile +8 -0
  148. data/mkdocs.yml +3 -3
  149. data/robot_concurrency.md +38 -0
  150. data/simple_acp_review.md +298 -0
  151. data/temp.md +6 -0
  152. data/tool_manifest_plan.md +155 -0
  153. metadata +82 -83
  154. data/docs/examples/rails-application.md +0 -419
  155. data/docs/guides/ractor-parallelism.md +0 -364
  156. data/docs/guides/rails-integration.md +0 -681
  157. data/docs/superpowers/plans/2026-04-14-ractor-integration.md +0 -1538
  158. data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +0 -258
  159. data/lib/generators/robot_lab/install_generator.rb +0 -90
  160. data/lib/generators/robot_lab/job_generator.rb +0 -40
  161. data/lib/generators/robot_lab/robot_generator.rb +0 -55
  162. data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -42
  163. data/lib/generators/robot_lab/templates/job.rb.tt +0 -21
  164. data/lib/generators/robot_lab/templates/migration.rb.tt +0 -32
  165. data/lib/generators/robot_lab/templates/result_model.rb.tt +0 -52
  166. data/lib/generators/robot_lab/templates/robot.rb.tt +0 -31
  167. data/lib/generators/robot_lab/templates/robot_job.rb.tt +0 -18
  168. data/lib/generators/robot_lab/templates/robot_test.rb.tt +0 -34
  169. data/lib/generators/robot_lab/templates/routing_robot.rb.tt +0 -59
  170. data/lib/generators/robot_lab/templates/thread_model.rb.tt +0 -40
  171. data/lib/robot_lab/document_store.rb +0 -155
  172. data/lib/robot_lab/ractor_boundary.rb +0 -42
  173. data/lib/robot_lab/ractor_job.rb +0 -37
  174. data/lib/robot_lab/ractor_memory_proxy.rb +0 -85
  175. data/lib/robot_lab/ractor_network_scheduler.rb +0 -154
  176. data/lib/robot_lab/ractor_worker_pool.rb +0 -117
  177. data/lib/robot_lab/rails_integration/engine.rb +0 -29
  178. data/lib/robot_lab/rails_integration/job.rb +0 -158
  179. data/lib/robot_lab/rails_integration/railtie.rb +0 -51
  180. data/lib/robot_lab/rails_integration/turbo_stream_callbacks.rb +0 -72
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 33: XYZZY Stock Price Predictor
5
+ #
6
+ # Consumes fake streaming prices for ticker XYZZY from a Redis channel,
7
+ # predicts the high and low over the next price window using an SMA + EMA
8
+ # ensemble, and uses a RobotLab learning robot to tune predictor parameters
9
+ # after each window closes.
10
+ #
11
+ # Run alongside:
12
+ # ruby examples/33_stock_generator.rb (in a separate terminal)
13
+ #
14
+ # Prerequisites:
15
+ # gem install redis
16
+ # Redis server running on localhost:6379
17
+ #
18
+ # Usage:
19
+ # ruby examples/33_stock_predictor.rb
20
+
21
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
22
+ require "robot_lab"
23
+ require "robot_lab/durable"
24
+ require "redis"
25
+ require "json"
26
+
27
+ CHANNEL = "stock:xyzzy"
28
+ WINDOW_SIZE = 12 # ticks per prediction window
29
+
30
+ # ── Mutable predictor parameters ──────────────────────────────────────────────
31
+
32
+ module PredictorConfig
33
+ @sma_window = 10
34
+ @sma_std_multiplier = 1.5
35
+ @ema_alpha = 0.2
36
+ @ema_vol_multiplier = 2.0
37
+ @sma_weight = 0.5
38
+
39
+ class << self
40
+ attr_accessor :sma_window, :sma_std_multiplier, :ema_alpha,
41
+ :ema_vol_multiplier, :sma_weight
42
+
43
+ def summary
44
+ format(
45
+ "sma_window=%d sma_std=%.2f ema_alpha=%.2f ema_vol=%.2f sma_weight=%.2f",
46
+ sma_window, sma_std_multiplier, ema_alpha, ema_vol_multiplier, sma_weight
47
+ )
48
+ end
49
+ end
50
+ end
51
+
52
+ # ── SMA predictor ──────────────────────────────────────────────────────────────
53
+
54
+ module SMAPredictor
55
+ def self.predict(prices)
56
+ window = prices.last(PredictorConfig.sma_window)
57
+ mean = window.sum / window.size.to_f
58
+ var = window.sum { |p| (p - mean)**2 } / window.size.to_f
59
+ std = Math.sqrt(var)
60
+ mult = PredictorConfig.sma_std_multiplier
61
+
62
+ {
63
+ high: (mean + mult * std).round(2),
64
+ low: [mean - mult * std, 1.0].max.round(2)
65
+ }
66
+ end
67
+ end
68
+
69
+ # ── EMA predictor (stateful — updated every tick) ──────────────────────────────
70
+
71
+ module EMAPredictor
72
+ @ema = nil
73
+ @var_ema = nil
74
+
75
+ class << self
76
+ def update(price)
77
+ alpha = PredictorConfig.ema_alpha
78
+ if @ema.nil?
79
+ @ema = price
80
+ @var_ema = 0.0
81
+ else
82
+ delta = price - @ema
83
+ @ema = alpha * price + (1 - alpha) * @ema
84
+ @var_ema = alpha * delta**2 + (1 - alpha) * @var_ema
85
+ end
86
+ end
87
+
88
+ def predict
89
+ return nil if @ema.nil?
90
+
91
+ vol = Math.sqrt(@var_ema) * PredictorConfig.ema_vol_multiplier
92
+ {
93
+ high: (@ema + vol).round(2),
94
+ low: [@ema - vol, 1.0].max.round(2)
95
+ }
96
+ end
97
+ end
98
+ end
99
+
100
+ # ── Ensemble predictor ─────────────────────────────────────────────────────────
101
+
102
+ module EnsemblePredictor
103
+ def self.predict(prices)
104
+ sma = SMAPredictor.predict(prices)
105
+ ema = EMAPredictor.predict
106
+ return sma unless ema
107
+
108
+ w = PredictorConfig.sma_weight
109
+ {
110
+ high: (w * sma[:high] + (1 - w) * ema[:high]).round(2),
111
+ low: (w * sma[:low] + (1 - w) * ema[:low]).round(2)
112
+ }
113
+ end
114
+ end
115
+
116
+ # ── AdjustParameters tool ──────────────────────────────────────────────────────
117
+
118
+ class AdjustParameters < RobotLab::Tool
119
+ description "Adjust one predictor parameter to improve future prediction accuracy. " \
120
+ "Make at most one or two targeted changes per window."
121
+
122
+ param :parameter, type: "string",
123
+ desc: "Parameter to adjust: sma_window, sma_std_multiplier, ema_alpha, ema_vol_multiplier, sma_weight"
124
+ param :value, type: "number",
125
+ desc: "New value (sma_window: 3-30 int; std/vol multipliers: 0.5-4.0; ema_alpha: 0.05-0.5; sma_weight: 0.0-1.0)"
126
+ param :reasoning, type: "string",
127
+ desc: "Why this change should reduce prediction error"
128
+
129
+ LIMITS = {
130
+ "sma_window" => { min: 3, max: 30, integer: true },
131
+ "sma_std_multiplier" => { min: 0.5, max: 4.0, integer: false },
132
+ "ema_alpha" => { min: 0.05, max: 0.5, integer: false },
133
+ "ema_vol_multiplier" => { min: 0.5, max: 4.0, integer: false },
134
+ "sma_weight" => { min: 0.0, max: 1.0, integer: false }
135
+ }.freeze
136
+
137
+ def execute(parameter:, value:, reasoning:)
138
+ spec = LIMITS[parameter]
139
+ return "Unknown parameter '#{parameter}'. Valid: #{LIMITS.keys.join(", ")}" unless spec
140
+
141
+ clamped = value.to_f.clamp(spec[:min], spec[:max])
142
+ clamped = clamped.round if spec[:integer]
143
+
144
+ PredictorConfig.send(:"#{parameter}=", clamped)
145
+
146
+ "Set #{parameter} = #{clamped}. #{reasoning}"
147
+ end
148
+ end
149
+
150
+ # ── Error metrics ──────────────────────────────────────────────────────────────
151
+
152
+ WindowResult = Data.define(
153
+ :window_num,
154
+ :predicted_high, :predicted_low,
155
+ :actual_high, :actual_low,
156
+ :high_err, :low_err, :mean_err
157
+ )
158
+
159
+ def evaluate_window(window_num, predicted, actuals)
160
+ actual_high = actuals.max.round(2)
161
+ actual_low = actuals.min.round(2)
162
+ high_err = (predicted[:high] - actual_high).abs.round(2)
163
+ low_err = (predicted[:low] - actual_low).abs.round(2)
164
+ mean_err = ((high_err + low_err) / 2.0).round(2)
165
+
166
+ WindowResult.new(
167
+ window_num:,
168
+ predicted_high: predicted[:high], predicted_low: predicted[:low],
169
+ actual_high:, actual_low:,
170
+ high_err:, low_err:, mean_err:
171
+ )
172
+ end
173
+
174
+ def tuner_prompt(result)
175
+ <<~PROMPT
176
+ Window #{result.window_num} just closed.
177
+
178
+ Prediction vs Actual:
179
+ Predicted: high=$#{result.predicted_high} low=$#{result.predicted_low}
180
+ Actual: high=$#{result.actual_high} low=$#{result.actual_low}
181
+ Error: high_err=$#{result.high_err} low_err=$#{result.low_err} mean_err=$#{result.mean_err}
182
+
183
+ Current parameters:
184
+ #{PredictorConfig.summary}
185
+
186
+ Window size: #{WINDOW_SIZE} ticks.
187
+
188
+ First call RecallKnowledge to check what has worked before.
189
+ Then decide whether to adjust a parameter via AdjustParameters.
190
+ If the error is acceptable or you are uncertain, do nothing.
191
+ If you notice a clear pattern worth preserving, call RecordKnowledge.
192
+ PROMPT
193
+ end
194
+
195
+ # ── Main ──────────────────────────────────────────────────────────────────────
196
+
197
+ puts "=" * 60
198
+ puts "XYZZY Stock Predictor"
199
+ puts "=" * 60
200
+ puts "Channel : #{CHANNEL}"
201
+ puts "Window : #{WINDOW_SIZE} ticks"
202
+ puts "Model : SMA + EMA Ensemble with Durable Learning"
203
+ puts "Warmup : #{PredictorConfig.sma_window} ticks"
204
+ puts "Press Ctrl-C to stop."
205
+ puts "-" * 60
206
+
207
+ redis = Redis.new
208
+ prices = []
209
+ robot = RobotLab.build(
210
+ model: "gpt-5.4",
211
+ name: "predictor_tuner",
212
+ system_prompt: <<~PROMPT,
213
+ You are a quantitative analyst tuning an ensemble stock price range
214
+ predictor for ticker XYZZY. Each prediction covers the high and low
215
+ price over the next #{WINDOW_SIZE} ticks.
216
+
217
+ The ensemble combines a Simple Moving Average (SMA) band and an
218
+ Exponential Moving Average (EMA) band. Adjustable parameters:
219
+
220
+ sma_window (3-30 int) — lookback period for SMA
221
+ sma_std_multiplier (0.5-4.0) — band width relative to SMA stddev
222
+ ema_alpha (0.05-0.5) — EMA smoothing (higher = more reactive)
223
+ ema_vol_multiplier (0.5-4.0) — band width relative to EMA volatility
224
+ sma_weight (0.0-1.0) — SMA share in ensemble (EMA = 1 - weight)
225
+
226
+ Workflow per window:
227
+ 1. Call RecallKnowledge to check past findings before acting.
228
+ 2. If the error is clearly too high/low in one direction, adjust the
229
+ relevant band multiplier via AdjustParameters.
230
+ 3. Make at most two adjustments per window to isolate cause and effect.
231
+ 4. If you observe a reliable pattern, call RecordKnowledge to preserve it.
232
+ 5. When uncertain, do nothing rather than guess.
233
+ PROMPT
234
+ local_tools: [AdjustParameters],
235
+ learn: true,
236
+ learn_domain: "xyzzy stock prediction"
237
+ )
238
+
239
+ warmed_up = false
240
+ pending_pred = nil # { prediction: {high:, low:}, window_prices: [] }
241
+ window_num = 0
242
+
243
+ trap("INT") { puts "\nPredictor stopped."; exit }
244
+
245
+ puts "Connecting to Redis and subscribing to #{CHANNEL}..."
246
+
247
+ redis.subscribe(CHANNEL) do |on|
248
+ on.message do |_channel, payload|
249
+ data = JSON.parse(payload, symbolize_names: true)
250
+ tick = data[:tick]
251
+ price = data[:price].to_f
252
+
253
+ EMAPredictor.update(price)
254
+ prices << price
255
+
256
+ # ── Warmup phase ──────────────────────────────────────────────
257
+ unless warmed_up
258
+ if prices.size < PredictorConfig.sma_window
259
+ puts "Tick %5d $%8.2f [warming up %d/%d]" % [tick, price, prices.size, PredictorConfig.sma_window]
260
+ next
261
+ end
262
+
263
+ warmed_up = true
264
+ pred = EnsemblePredictor.predict(prices)
265
+ pending_pred = { prediction: pred, window_prices: [] }
266
+
267
+ puts "Tick %5d $%8.2f [warmup done]" % [tick, price]
268
+ puts " First prediction → high=$#{pred[:high]} low=$#{pred[:low]}"
269
+ next
270
+ end
271
+
272
+ # ── Accumulate current window ──────────────────────────────────
273
+ pending_pred[:window_prices] << price
274
+ progress = pending_pred[:window_prices].size
275
+ pred = pending_pred[:prediction]
276
+
277
+ puts "Tick %5d $%8.2f [%2d/#{WINDOW_SIZE}] (pred high=$#{pred[:high]} low=$#{pred[:low]})" %
278
+ [tick, price, progress]
279
+
280
+ next unless progress >= WINDOW_SIZE
281
+
282
+ # ── Window closed — evaluate ───────────────────────────────────
283
+ window_num += 1
284
+ result = evaluate_window(window_num, pred, pending_pred[:window_prices])
285
+
286
+ puts "\n#{"─" * 60}"
287
+ puts " Window #{result.window_num} result:"
288
+ puts " Predicted high=$%-8.2f low=$%-.2f" % [result.predicted_high, result.predicted_low]
289
+ puts " Actual high=$%-8.2f low=$%-.2f" % [result.actual_high, result.actual_low]
290
+ puts " Error high=%-8.2f low=%-8.2f mean=%.2f" % [result.high_err, result.low_err, result.mean_err]
291
+ puts "#{"─" * 60}"
292
+
293
+ print " [tuner] analyzing window #{window_num}..."
294
+ tuner_response = robot.run(tuner_prompt(result))
295
+ tuner_line = tuner_response.reply.lines.first&.chomp || "(no response)"
296
+ puts "\r [tuner] #{tuner_line}#{" " * 20}"
297
+ puts " Params: #{PredictorConfig.summary}"
298
+ puts
299
+
300
+ # ── Start next window ──────────────────────────────────────────
301
+ new_pred = EnsemblePredictor.predict(prices)
302
+ pending_pred = { prediction: new_pred, window_prices: [] }
303
+ puts " Next prediction → high=$#{new_pred[:high]} low=$#{new_pred[:low]}"
304
+ puts
305
+ end
306
+ end
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 34: AgentSkills.io Integration
5
+ #
6
+ # Demonstrates the unified skills: param detecting AgentSkills folder format.
7
+ # Skills in ~/.prompts/skills/ are matched at runtime via embedding similarity
8
+ # before each run() call — only relevant skills are injected.
9
+ #
10
+ # Usage:
11
+ # mkdir -p ~/.prompts/skills/code_reviewer
12
+ # # (create SKILL.md as shown in the example header)
13
+ # ANTHROPIC_API_KEY=your_key ruby examples/34_agentskills.rb
14
+
15
+ require_relative "common"
16
+
17
+ require "logger"
18
+ log_file = File.join(__dir__, "34.log")
19
+ RobotLab.config.logger = Logger.new(log_file)
20
+ RubyLLM.configure { |c| c.logger = Logger.new(log_file) }
21
+
22
+ banner "RobotLab — AgentSkills.io Integration Demo"
23
+
24
+ # Check if the skill is installed
25
+ skill_path = File.expand_path("~/.prompts/skills/code_reviewer/SKILL.md")
26
+ unless File.exist?(skill_path)
27
+ puts "Demo skill not found at #{skill_path}"
28
+ puts "Create it with:"
29
+ puts " mkdir -p ~/.prompts/skills/code_reviewer"
30
+ puts " # Then add SKILL.md with name: code_reviewer"
31
+ exit 1
32
+ end
33
+
34
+ # Build a robot that lists code_reviewer as a candidate skill.
35
+ # At runtime, if the user message is semantically similar to
36
+ # "Review Ruby code for quality, style, and potential bugs",
37
+ # the skill's instructions are injected into the system prompt.
38
+ robot = RobotLab.build(
39
+ model: LLM[:default].model,
40
+ name: "assistant",
41
+ system_prompt: "You are a helpful Ruby programming assistant.",
42
+ skills: [:code_reviewer]
43
+ )
44
+
45
+ puts "Pending AgentSkills: #{robot.instance_variable_get(:@pending_agent_skills).map(&:name).inspect}"
46
+ puts
47
+
48
+ # Message semantically related to code review — skill should activate
49
+ code_question = <<~MSG
50
+ Please review this Ruby method for quality issues:
51
+
52
+ def process(data)
53
+ begin
54
+ result = data.map { |item| transform(item) }
55
+ save(result)
56
+ rescue => e
57
+ puts e.message
58
+ end
59
+ end
60
+ MSG
61
+
62
+ puts "Query: code review (skill should activate)"
63
+ result = robot.run(code_question)
64
+ puts result.reply
65
+ puts
66
+ hr
67
+
68
+ # Message unrelated to code review — skill should NOT activate
69
+ puts "Query: general question (skill should NOT activate)"
70
+ result = robot.run("What is the capital of France?")
71
+ puts result.reply
72
+ puts
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 35: Hooks Architecture with robot_lab-xyzzy
5
+ #
6
+ # Demonstrates the hook handler class pattern. Hooks are named classes that
7
+ # inherit from RobotLab::Hook and implement class methods for each lifecycle
8
+ # phase they care about. All hooks are Ractor-safe by default.
9
+ #
10
+ # Usage:
11
+ # bundle exec ruby examples/35_hooks.rb
12
+
13
+ require_relative "common"
14
+ require_relative "xyzzy"
15
+
16
+ XYZZY_LOG = File.join(__dir__, "xyzzy_hooks.log")
17
+ RobotLab::Xyzzy.logger = Logger.new(XYZZY_LOG)
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Handler classes used in this demo
21
+ # ---------------------------------------------------------------------------
22
+
23
+ class RuntimeDemoHook < RobotLab::Hook
24
+ self.namespace = :runtime_demo
25
+
26
+ def self.after_run(ctx)
27
+ ctx.local.reply_seen = ctx.response.reply
28
+ end
29
+ end
30
+
31
+ class LocalDemoHook < RobotLab::Hook
32
+ self.namespace = :local_demo
33
+
34
+ def self.before_run(ctx)
35
+ ctx.local.note = "local robot hook state"
36
+ ctx.local.demo = "robot_run"
37
+ end
38
+ end
39
+
40
+ class PerfHook < RobotLab::Hook
41
+ self.namespace = :perf
42
+ @timings = []
43
+ @mutex = Mutex.new
44
+
45
+ class << self
46
+ def reset!
47
+ @mutex.synchronize { @timings = [] }
48
+ end
49
+
50
+ def timings
51
+ @mutex.synchronize { @timings.dup }
52
+ end
53
+
54
+ def around_run(ctx, &block)
55
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
56
+ result = block.call
57
+ elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1_000).round(3)
58
+ @mutex.synchronize { @timings << elapsed }
59
+ puts " [perf] #{ctx.request.inspect} — #{elapsed}ms"
60
+ result
61
+ end
62
+ end
63
+ end
64
+
65
+ class GenerationTracerHook < RobotLab::Hook
66
+ self.namespace = :tracer
67
+
68
+ def self.before_llm_generation(ctx)
69
+ puts " [tracer] llm_generation start request=#{ctx.request.inspect}"
70
+ end
71
+
72
+ def self.after_llm_generation(ctx)
73
+ msg = ctx.generation_response
74
+ puts " [tracer] llm_generation done model=#{msg&.model_id} tokens=#{msg&.output_tokens}"
75
+ end
76
+ end
77
+
78
+ class GenCacheHook < RobotLab::Hook
79
+ self.namespace = :gen_cache
80
+ @cache = {}
81
+ @mutex = Mutex.new
82
+ @call_count = 0
83
+
84
+ class << self
85
+ def reset!
86
+ @mutex.synchronize do
87
+ @cache = {}
88
+ @call_count = 0
89
+ end
90
+ end
91
+
92
+ def call_count
93
+ @mutex.synchronize { @call_count }
94
+ end
95
+
96
+ def around_llm_generation(ctx, &block)
97
+ cached = @mutex.synchronize { @cache[ctx.request] }
98
+ if cached
99
+ puts " [gen_cache] HIT — skipping LLM"
100
+ cached
101
+ else
102
+ puts " [gen_cache] MISS — calling LLM"
103
+ @mutex.synchronize { @call_count += 1 }
104
+ block.call.tap { |r| @mutex.synchronize { @cache[ctx.request] = r } }
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ class NetworkDemoHook < RobotLab::Hook
111
+ self.namespace = :network_demo
112
+
113
+ def self.before_task(ctx)
114
+ ctx.local.task_note = "network-scoped task hook"
115
+ end
116
+ end
117
+
118
+ # ---------------------------------------------------------------------------
119
+
120
+ HookDemoResponse = Data.define(:content, :tool_calls, :stop_reason) do
121
+ def initialize(content:, tool_calls: nil, stop_reason: "end_turn")
122
+ super
123
+ end
124
+ end
125
+
126
+ class HookDemoTool < RobotLab::Tool
127
+ description "Returns a deterministic hook demo value"
128
+ param :label, type: "string", desc: "The label to echo"
129
+
130
+ def execute(label:)
131
+ { label: label, status: "handled by HookDemoTool" }
132
+ end
133
+ end
134
+
135
+ class HookDemo
136
+ def run
137
+ banner "Hooks Architecture with robot_lab-xyzzy"
138
+
139
+ explain_setup
140
+ run_robot
141
+ run_llm_loop
142
+ run_tool
143
+ run_network
144
+ run_error
145
+
146
+ hr
147
+ puts "Hook demo complete."
148
+ end
149
+
150
+ private
151
+
152
+ def explain_setup
153
+ section "Extension Registration"
154
+ puts <<~TEXT
155
+ xyzzy.rb is a single-file hook extension loaded from examples/.
156
+
157
+ It is a RobotLab::Hook subclass registered under namespace :#{RobotLab::Xyzzy.namespace}.
158
+ stdout : [xyzzy] timestamp hook_name (one line per call)
159
+ logfile : full context snapshot → #{XYZZY_LOG}
160
+ TEXT
161
+ end
162
+
163
+ def run_robot
164
+ section "Robot Run Hooks"
165
+ robot = hooked_robot("hook_demo_robot")
166
+ robot.on(LocalDemoHook)
167
+
168
+ result = robot.run("show the hook pipeline", hooks: RuntimeDemoHook)
169
+
170
+ puts "Robot reply: #{result.reply}"
171
+ end
172
+
173
+ def run_llm_loop
174
+ section "LLM Request/Response Loop"
175
+ puts <<~TEXT
176
+ Demonstrates before/around/after_llm_generation hooks.
177
+ GenCacheHook implements around_llm_generation: on a cache hit the block
178
+ (the real LLM call) is never invoked. PerfHook times each robot.run call.
179
+ TEXT
180
+
181
+ GenCacheHook.reset!
182
+ PerfHook.reset!
183
+
184
+ puts " provider=#{LLM[:default].provider} model=#{LLM[:default].model}\n\n"
185
+
186
+ robot = RobotLab.build(
187
+ name: "loop_demo_robot",
188
+ system_prompt: "You are a helpful assistant. Answer every question in one concise sentence."
189
+ ).with_model(LLM[:default].model)
190
+
191
+ robot.on(PerfHook)
192
+ robot.on(GenerationTracerHook)
193
+ robot.on(GenCacheHook)
194
+
195
+ queries = [
196
+ "what is the boiling point of water?",
197
+ "what is 6 * 7?",
198
+ "what is the boiling point of water?",
199
+ "what is 6 * 7?"
200
+ ]
201
+
202
+ queries.each_with_index do |query, i|
203
+ puts "\n--- query #{i + 1}: #{query.inspect} ---"
204
+ result = robot.run(query)
205
+ puts " reply: #{result.reply}"
206
+ end
207
+
208
+ timings = PerfHook.timings
209
+ cache_hits = queries.size - GenCacheHook.call_count
210
+ avg_ms = (timings.sum / timings.size).round(3)
211
+ puts "\n--- summary ---"
212
+ puts " robot.run calls : #{queries.size}"
213
+ puts " LLM calls made : #{GenCacheHook.call_count} (#{cache_hits} served from gen_cache)"
214
+ puts " avg run time : #{avg_ms}ms"
215
+ end
216
+
217
+ def run_tool
218
+ section "Tool Call Hooks"
219
+ tool = HookDemoTool.new
220
+ result = tool.call({ "label" => "tool hook payload" })
221
+ puts "Tool result: #{result.inspect}"
222
+ end
223
+
224
+ def run_network
225
+ section "Network and Task Hooks"
226
+ network = RobotLab::Network.new(name: "hook_demo_network")
227
+ network.on(NetworkDemoHook)
228
+ network.task(:summarize, hooked_robot("network_worker"), depends_on: :none)
229
+
230
+ result = network.run(message: "summarize hook activity")
231
+ reply = result.respond_to?(:reply) ? result.reply : result&.value&.reply
232
+ puts "Network result: #{reply}"
233
+ end
234
+
235
+ def run_error
236
+ section "Error Hook"
237
+ failing_robot = RobotLab.build(name: "failing_hook_robot", system_prompt: "raise")
238
+ failing_robot.instance_variable_get(:@chat).define_singleton_method(:ask) do |_message = nil, **_kwargs|
239
+ raise "planned hook demo failure"
240
+ end
241
+
242
+ failing_robot.run("trigger the on_error hook")
243
+ rescue RuntimeError => e
244
+ puts "Caught expected error: #{e.message}"
245
+ end
246
+
247
+ def hooked_robot(name)
248
+ RobotLab.build(name: name, system_prompt: "deterministic hook demo").tap do |robot|
249
+ robot.instance_variable_get(:@chat).define_singleton_method(:ask) do |message = nil, **_kwargs, &_block|
250
+ HookDemoResponse.new(content: "deterministic response to #{message.inspect}")
251
+ end
252
+ end
253
+ end
254
+ end
255
+
256
+ HookDemo.new.run
data/examples/README.md CHANGED
@@ -58,6 +58,7 @@ examples/
58
58
  29_ractor_tools.rb # Ractor-safe tools: worker pool, freeze_deep, parallel batch
59
59
  30_ractor_network.rb # Ractor network scheduler: dependency waves, parallel_mode
60
60
  31_launch_assessment.rb # 6 parallel analysts, max_concurrent_robots: 4 semaphore cap
61
+ 35_hooks.rb # Hook architecture demo using robot_lab-xyzzy
61
62
  18_rails/ # Minimal Rails 8 demo app (full integration)
62
63
  app/robots/chat_robot.rb # Robot factory with system prompt + TimeTool
63
64
  app/tools/time_tool.rb # Custom RobotLab::Tool subclass
@@ -226,7 +227,7 @@ Demonstrates `robot.search_history(query, limit:)` — semantic search over accu
226
227
 
227
228
  Demonstrates `memory.store_document(key, text)` and `memory.search_documents(query, limit:)` — a lightweight RAG store using `fastembed` (BAAI/bge-small-en-v1.5). Documents are embedded once; queries are compared by cosine similarity at search time. Includes the `RobotLab::DocumentStore` standalone API and a RAG pattern sketch showing how to pass retrieved context to a robot.
228
229
 
229
- **Requires:** `fastembed` gem (already a core dependency); downloads the ~23 MB ONNX model on first run (cached in `~/.cache/fastembed/`)
230
+ **Requires:** `robot_lab-document_store` gem (`gem "robot_lab-document_store"` in your Gemfile); downloads the ~23 MB ONNX model on first run (cached in `~/.cache/fastembed/`)
230
231
 
231
232
  ### 27 — Production Incident War Room
232
233
 
@@ -306,6 +307,12 @@ Demonstrates: `max_concurrent_robots:` on `RunConfig`, `Async::Semaphore` back-p
306
307
 
307
308
  **Requires:** LLM API key
308
309
 
310
+ ### 35 — Hooks Architecture
311
+
312
+ Loads the local `robot_lab-xyzzy` extension, which registers for every hook and logs each callback with the context it receives. Demonstrates robot run, LLM generation, tool call, network run, task, and error hooks with deterministic stubbed responses.
313
+
314
+ **Requires:** None (no LLM calls)
315
+
309
316
  ### 18 — Rails Integration Demo
310
317
 
311
318
  A minimal, hand-built Rails 8 app that exercises every piece of RobotLab's Rails integration end-to-end. No `rails new` — every file is hand-crafted for minimum size.