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.
- checksums.yaml +4 -4
- data/.architecture/AGENTS.md +32 -0
- data/.architecture/config.yml +8 -0
- data/.architecture/members.yml +60 -0
- data/.architecture/reviews/feature-free-will.md +490 -0
- data/.architecture/reviews/overall-codebase.md +427 -0
- data/.claude/settings.local.json +57 -0
- data/.codex/config.toml +2 -0
- data/.envrc +1 -0
- data/.irbrc +2 -2
- data/.loki +60 -0
- data/.quality/reek_baseline.txt +43 -0
- data/.rubocop.yml +10 -0
- data/CHANGELOG.md +126 -0
- data/CLAUDE.md +139 -0
- data/README.md +130 -73
- data/Rakefile +131 -9
- data/agent2agent_review.md +192 -0
- data/agentf_improvements.md +253 -0
- data/agents.md +14 -0
- data/docs/api/core/index.md +1 -0
- data/docs/api/core/network.md +31 -1
- data/docs/api/core/robot.md +91 -5
- data/docs/api/errors.md +47 -2
- data/docs/api/index.md +3 -0
- data/docs/architecture/core-concepts.md +40 -0
- data/docs/architecture/network-orchestration.md +8 -0
- data/docs/architecture/robot-execution.md +1 -1
- data/docs/examples/index.md +37 -2
- data/docs/getting-started/configuration.md +39 -7
- data/docs/guides/building-robots.md +39 -1
- data/docs/guides/creating-networks.md +6 -1
- data/docs/guides/hooks.md +997 -0
- data/docs/guides/index.md +19 -14
- data/docs/guides/knowledge.md +9 -3
- data/docs/guides/observability.md +206 -7
- data/docs/guides/using-tools.md +69 -0
- data/docs/index.md +31 -4
- data/docs/superpowers/plans/2026-05-06-agentskills.md +1303 -0
- data/docs/superpowers/specs/2026-05-06-agentskills-design.md +247 -0
- data/examples/.envrc +1 -0
- data/examples/01_simple_robot.rb +5 -9
- data/examples/02_tools.rb +5 -9
- data/examples/03_network.rb +18 -10
- data/examples/04_mcp.rb +21 -29
- data/examples/05_streaming.rb +12 -18
- data/examples/06_prompt_templates.rb +11 -19
- data/examples/07_network_memory.rb +16 -31
- data/examples/08_llm_config.rb +10 -22
- data/examples/09_chaining.rb +16 -27
- data/examples/10_memory.rb +12 -28
- data/examples/11_network_introspection.rb +15 -29
- data/examples/12_message_bus.rb +5 -12
- data/examples/13_spawn.rb +5 -10
- data/examples/14_rusty_circuit/.envrc +1 -0
- data/examples/14_rusty_circuit/comic.rb +2 -0
- data/examples/14_rusty_circuit/heckler.rb +1 -1
- data/examples/14_rusty_circuit/open_mic.rb +1 -3
- data/examples/14_rusty_circuit/scout.rb +2 -0
- data/examples/15_memory_network_and_bus/.envrc +1 -0
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +6 -3
- data/examples/15_memory_network_and_bus/linux_writer.rb +1 -1
- data/examples/15_memory_network_and_bus/output/combined_article.md +6 -6
- data/examples/15_memory_network_and_bus/output/final_article.md +6 -8
- data/examples/15_memory_network_and_bus/output/linux_draft.md +4 -2
- data/examples/15_memory_network_and_bus/output/mac_draft.md +3 -3
- data/examples/15_memory_network_and_bus/output/memory.json +6 -6
- data/examples/15_memory_network_and_bus/output/revision_1.md +10 -11
- data/examples/15_memory_network_and_bus/output/revision_2.md +6 -8
- data/examples/15_memory_network_and_bus/output/windows_draft.md +3 -3
- data/examples/16_writers_room/.envrc +1 -0
- data/examples/16_writers_room/writers_room.rb +2 -4
- data/examples/17_skills.rb +8 -17
- data/examples/18_rails/Gemfile +1 -0
- data/examples/19_token_tracking.rb +9 -15
- data/examples/20_circuit_breaker.rb +10 -19
- data/examples/21_learning_loop.rb +11 -20
- data/examples/22_context_compression.rb +6 -13
- data/examples/23_convergence.rb +6 -17
- data/examples/24_structured_delegation.rb +11 -15
- data/examples/25_history_search.rb +5 -12
- data/examples/26_document_store.rb +6 -13
- data/examples/27_incident_response/incident_response.rb +4 -5
- data/examples/28_mcp_discovery.rb +8 -11
- data/examples/29_ractor_tools.rb +4 -9
- data/examples/30_ractor_network.rb +10 -19
- data/examples/31_launch_assessment.rb +10 -23
- data/examples/32_newsletter_reader.rb +188 -0
- data/examples/33_stock_generator.rb +80 -0
- data/examples/33_stock_predictor.rb +306 -0
- data/examples/34_agentskills.rb +72 -0
- data/examples/35_hooks.rb +256 -0
- data/examples/README.md +8 -1
- data/examples/common.rb +80 -0
- data/examples/ruboruby.md +423 -0
- data/examples/xyzzy.rb +90 -0
- data/lib/robot_lab/agent_skill.rb +64 -0
- data/lib/robot_lab/agent_skill_catalog.rb +74 -0
- data/lib/robot_lab/ask_user.rb +2 -2
- data/lib/robot_lab/budget/ledger.rb +98 -0
- data/lib/robot_lab/bus_poller.rb +12 -5
- data/lib/robot_lab/capabilities.rb +84 -0
- data/lib/robot_lab/config/defaults.yml +10 -0
- data/lib/robot_lab/config.rb +5 -16
- data/lib/robot_lab/delegation_future.rb +1 -1
- data/lib/robot_lab/doom_loop_detector.rb +98 -0
- data/lib/robot_lab/error.rb +34 -4
- data/lib/robot_lab/errors.rb +45 -0
- data/lib/robot_lab/history_compressor.rb +4 -10
- data/lib/robot_lab/hook.rb +79 -0
- data/lib/robot_lab/hook_context.rb +194 -0
- data/lib/robot_lab/hook_registry.rb +55 -0
- data/lib/robot_lab/hooks.rb +87 -0
- data/lib/robot_lab/mcp/client.rb +1 -2
- data/lib/robot_lab/mcp/connection_poller.rb +5 -5
- data/lib/robot_lab/mcp/server.rb +1 -1
- data/lib/robot_lab/mcp/server_discovery.rb +0 -2
- data/lib/robot_lab/memory.rb +32 -27
- data/lib/robot_lab/memory_change.rb +2 -2
- data/lib/robot_lab/message.rb +4 -4
- data/lib/robot_lab/narrator.rb +87 -0
- data/lib/robot_lab/network.rb +87 -25
- data/lib/robot_lab/robot/agent_skill_matching.rb +99 -0
- data/lib/robot_lab/robot/budget.rb +89 -0
- data/lib/robot_lab/robot/bus_messaging.rb +79 -33
- data/lib/robot_lab/robot/history_search.rb +4 -1
- data/lib/robot_lab/robot/hooking.rb +56 -0
- data/lib/robot_lab/robot/mcp_management.rb +12 -12
- data/lib/robot_lab/robot/template_rendering.rb +62 -42
- data/lib/robot_lab/robot.rb +482 -242
- data/lib/robot_lab/robot_result.rb +6 -5
- data/lib/robot_lab/run_config.rb +27 -14
- data/lib/robot_lab/runnable.rb +51 -0
- data/lib/robot_lab/sandbox/null.rb +13 -0
- data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
- data/lib/robot_lab/sandbox.rb +52 -0
- data/lib/robot_lab/script_tool.rb +134 -0
- data/lib/robot_lab/state_proxy.rb +7 -5
- data/lib/robot_lab/task.rb +26 -20
- data/lib/robot_lab/tool.rb +54 -13
- data/lib/robot_lab/tool_config.rb +1 -1
- data/lib/robot_lab/tool_manifest.rb +5 -7
- data/lib/robot_lab/user_message.rb +2 -2
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab/waiter.rb +1 -1
- data/lib/robot_lab.rb +89 -51
- data/logfile +8 -0
- data/mkdocs.yml +3 -3
- data/robot_concurrency.md +38 -0
- data/simple_acp_review.md +298 -0
- data/temp.md +6 -0
- data/tool_manifest_plan.md +155 -0
- metadata +82 -83
- data/docs/examples/rails-application.md +0 -419
- data/docs/guides/ractor-parallelism.md +0 -364
- data/docs/guides/rails-integration.md +0 -681
- data/docs/superpowers/plans/2026-04-14-ractor-integration.md +0 -1538
- data/docs/superpowers/specs/2026-04-14-ractor-integration-design.md +0 -258
- data/lib/generators/robot_lab/install_generator.rb +0 -90
- data/lib/generators/robot_lab/job_generator.rb +0 -40
- data/lib/generators/robot_lab/robot_generator.rb +0 -55
- data/lib/generators/robot_lab/templates/initializer.rb.tt +0 -42
- data/lib/generators/robot_lab/templates/job.rb.tt +0 -21
- data/lib/generators/robot_lab/templates/migration.rb.tt +0 -32
- data/lib/generators/robot_lab/templates/result_model.rb.tt +0 -52
- data/lib/generators/robot_lab/templates/robot.rb.tt +0 -31
- data/lib/generators/robot_lab/templates/robot_job.rb.tt +0 -18
- data/lib/generators/robot_lab/templates/robot_test.rb.tt +0 -34
- data/lib/generators/robot_lab/templates/routing_robot.rb.tt +0 -59
- data/lib/generators/robot_lab/templates/thread_model.rb.tt +0 -40
- data/lib/robot_lab/document_store.rb +0 -155
- data/lib/robot_lab/ractor_boundary.rb +0 -42
- data/lib/robot_lab/ractor_job.rb +0 -37
- data/lib/robot_lab/ractor_memory_proxy.rb +0 -85
- data/lib/robot_lab/ractor_network_scheduler.rb +0 -154
- data/lib/robot_lab/ractor_worker_pool.rb +0 -117
- data/lib/robot_lab/rails_integration/engine.rb +0 -29
- data/lib/robot_lab/rails_integration/job.rb +0 -158
- data/lib/robot_lab/rails_integration/railtie.rb +0 -51
- 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:** `
|
|
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.
|